cmd/gitbayd/adminusers.go

92a519ae8ad2301869b17704fe0c334e1039e8b2
gitbay/cmd/gitbayd/adminusers.go history · blame · raw

187 lines · 5151 bytes

  1package main
  2
  3import (
  4	"fmt"
  5
  6	"github.com/spf13/cobra"
  7
  8	"gitbay.org/gitbay/internal/config"
  9	"gitbay.org/gitbay/internal/control"
 10	"gitbay.org/gitbay/internal/gitutil"
 11	"gitbay.org/gitbay/internal/store"
 12)
 13
 14func withUser(use, short string, run func(st *store.Store, u store.User) error) *cobra.Command {
 15	return &cobra.Command{
 16		Use:   use + " <username>",
 17		Short: short,
 18		Args:  cobra.ExactArgs(1),
 19		RunE: func(cmd *cobra.Command, args []string) error {
 20			cfg, err := config.Load(configPath)
 21			if err != nil {
 22				return err
 23			}
 24			st, err := openStore(cfg)
 25			if err != nil {
 26				return err
 27			}
 28			defer st.Close()
 29			u, err := st.UserByUsername(args[0])
 30			if err != nil {
 31				return fmt.Errorf("no user %q", args[0])
 32			}
 33			return run(st, u)
 34		},
 35	}
 36}
 37
 38func adminUserDisableCmd() *cobra.Command {
 39	return withUser("disable", "suspend an account: keys and sessions refused until re-enabled",
 40		func(st *store.Store, u store.User) error {
 41			if err := st.SetUserDisabled(u.ID, true); err != nil {
 42				return err
 43			}
 44			st.Audit(0, "admin user.disabled", map[string]any{"user": u.Username})
 45			fmt.Printf("disabled %s: SSH, web sessions, and API tokens are refused; nothing was deleted\n", u.Username)
 46			return nil
 47		})
 48}
 49
 50func adminUserDeleteCmd() *cobra.Command {
 51	var yes bool
 52	cmd := withUser("delete", "delete an account that anchors nothing (keys, emails, and sessions go with it)",
 53		func(st *store.Store, u store.User) error {
 54			if !yes {
 55				return fmt.Errorf("deletion is permanent; pass --yes")
 56			}
 57			if err := st.DeleteUser(u.ID); err != nil {
 58				return err
 59			}
 60			st.Audit(0, "admin user.deleted", map[string]any{"user": u.Username})
 61			fmt.Printf("deleted %s\n", u.Username)
 62			return nil
 63		})
 64	cmd.Flags().BoolVar(&yes, "yes", false, "confirm permanent deletion")
 65	return cmd
 66}
 67
 68func adminUserEnableCmd() *cobra.Command {
 69	return withUser("enable", "restore a suspended account",
 70		func(st *store.Store, u store.User) error {
 71			if err := st.SetUserDisabled(u.ID, false); err != nil {
 72				return err
 73			}
 74			st.Audit(0, "admin user.enabled", map[string]any{"user": u.Username})
 75			fmt.Printf("enabled %s\n", u.Username)
 76			return nil
 77		})
 78}
 79
 80// adminMigrateCommitRefsCmd is a one-shot backfill: legacy commit-reference
 81// comments (author-attributed, bare sha) become system messages with a
 82// linked sha. Idempotent.
 83func adminMigrateCommitRefsCmd() *cobra.Command {
 84	return &cobra.Command{
 85		Use:   "migrate-commit-refs",
 86		Short: "convert legacy commit-reference comments into linked system messages",
 87		RunE: func(cmd *cobra.Command, args []string) error {
 88			cfg, err := config.Load(configPath)
 89			if err != nil {
 90				return err
 91			}
 92			st, err := openStore(cfg)
 93			if err != nil {
 94				return err
 95			}
 96			defer st.Close()
 97			n, err := st.MigrateCommitRefComments()
 98			if err != nil {
 99				return err
100			}
101			fmt.Printf("converted %d commit-reference comment(s) to system messages\n", n)
102			return nil
103		},
104	}
105}
106
107// adminBackfillActivityCmd walks every repo's default branch and records
108// commit activity for commits authored by verified addresses. Idempotent:
109// (repo, sha) dedup makes re-runs free. Imported history keeps its real
110// author dates, so migrated repos light up their actual timeline.
111func adminBackfillActivityCmd() *cobra.Command {
112	var perRepo int
113	cmd := &cobra.Command{
114		Use:   "backfill-activity",
115		Short: "record commit activity for existing default-branch history",
116		RunE: func(cmd *cobra.Command, args []string) error {
117			cfg, err := config.Load(configPath)
118			if err != nil {
119				return err
120			}
121			st, err := openStore(cfg)
122			if err != nil {
123				return err
124			}
125			defer st.Close()
126			repos, err := st.ListAllRepos()
127			if err != nil {
128				return err
129			}
130			total := 0
131			for _, r := range repos {
132				dir := control.RepoDir(cfg.Server.Root, r.OwnerName, r.Name)
133				authors, err := gitutil.RevListAuthors(dir, "", r.DefaultBranch, perRepo)
134				if err != nil {
135					continue // empty repo or missing branch
136				}
137				n := 0
138				for _, a := range authors {
139					if uid, ok := st.UserIDByVerifiedEmail(a.Email); ok {
140						if st.RecordCommitActivity(r.ID, a.SHA, uid, a.Day) {
141							n++
142						}
143					}
144				}
145				total += n
146				fmt.Printf("%s\t%d attributed\n", r.Path(), n)
147			}
148			fmt.Printf("total\t%d commits recorded\n", total)
149			return nil
150		},
151	}
152	cmd.Flags().IntVar(&perRepo, "per-repo", 20000, "max commits walked per repository")
153	return cmd
154}
155
156func adminAuditCmd() *cobra.Command {
157	var limit int
158	cmd := &cobra.Command{
159		Use:   "audit",
160		Short: "print the security audit log, newest first",
161		RunE: func(cmd *cobra.Command, args []string) error {
162			cfg, err := config.Load(configPath)
163			if err != nil {
164				return err
165			}
166			st, err := openStore(cfg)
167			if err != nil {
168				return err
169			}
170			defer st.Close()
171			entries, err := st.AuditEntries(limit)
172			if err != nil {
173				return err
174			}
175			for _, e := range entries {
176				actor := e.Actor
177				if actor == "" {
178					actor = "-"
179				}
180				fmt.Printf("%s\t%s\t%s\t%s\n", e.CreatedAt, actor, e.Action, e.Data)
181			}
182			return nil
183		},
184	}
185	cmd.Flags().IntVar(&limit, "limit", 100, "entries to print")
186	return cmd
187}