cmd/gitbayd/adminusers.go
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}