internal/control/org.go
319 lines · 9549 bytes
10 symbols in this file
1package control
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9
10 "gitbay.org/gitbay/internal/policy"
11 "gitbay.org/gitbay/internal/protocol"
12 "gitbay.org/gitbay/internal/store"
13)
14
15func init() {
16 register(Command{Path: []string{"org", "create"},
17 Summary: "create an organization (you become its first admin)",
18 Usage: "org create <name>",
19 Examples: []string{"org create krz"}, Run: runOrgCreate})
20 register(Command{Path: []string{"org", "list"},
21 Summary: "list organizations you belong to",
22 Usage: "org list",
23 Examples: []string{"org list"}, ReadOnly: true, Run: runOrgList})
24 register(Command{Path: []string{"org", "show"},
25 Summary: "show an organization and its members",
26 Usage: "org show <name>",
27 Examples: []string{"org show krz"}, ReadOnly: true, Run: runOrgShow})
28 register(Command{Path: []string{"org", "rename"},
29 Summary: "rename an organization",
30 Usage: "org rename <old> <new> (clone URLs change)",
31 Examples: []string{"org rename krz krazywarez"}, Run: runOrgRename})
32 register(Command{Path: []string{"org", "delete"},
33 Summary: "delete an empty organization",
34 Usage: "org delete <name> --yes",
35 Flags: []Flag{
36 {"--yes", "", "confirm the permanent delete", ""},
37 },
38 Examples: []string{"org delete krz --yes"}, Run: runOrgDelete})
39 register(Command{Path: []string{"org", "members", "add"},
40 NeedsRecentSignIn: true,
41 Summary: "add or update a member",
42 Usage: "org members add <org> <user> [--role member|admin]",
43 Flags: []Flag{
44 {"--role", "member|admin", "the member's role", "member"},
45 },
46 Examples: []string{"org members add krz cmc --role admin"}, Run: runOrgMembersAdd})
47 register(Command{Path: []string{"org", "members", "remove"},
48 Summary: "remove a member",
49 Usage: "org members remove <org> <user>",
50 Examples: []string{"org members remove krz cmc"}, Run: runOrgMembersRemove})
51 register(Command{Path: []string{"org", "members", "list"},
52 Summary: "list members",
53 Usage: "org members list <org>",
54 Examples: []string{"org members list krz"}, ReadOnly: true, Run: runOrgMembersList})
55}
56
57// orgAdmin loads an org and requires the caller to be one of its admins.
58func orgAdmin(c *Ctx, name string) (store.Org, int) {
59 org, err := c.Store.OrgByName(name)
60 if errors.Is(err, store.ErrNotFound) {
61 return org, c.fail(protocol.ExitNotFound, "no organization %q", name)
62 }
63 if err != nil {
64 return org, c.fail(protocol.ExitFailure, "%v", err)
65 }
66 role, err := c.Store.OrgRole(org.ID, c.User.ID)
67 if err != nil {
68 return org, c.fail(protocol.ExitFailure, "%v", err)
69 }
70 if role != "admin" {
71 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that; ask one", name)
72 }
73 return org, -1
74}
75
76func runOrgCreate(c *Ctx, args []string) int {
77 if len(args) != 1 {
78 return c.usage()
79 }
80 if err := policy.ValidateOwnerName(args[0]); err != nil {
81 return c.failInput(err)
82 }
83 orgCreateMu.Lock()
84 if code := checkOrgQuota(c); code >= 0 {
85 orgCreateMu.Unlock()
86 return code
87 }
88 _, err := c.Store.CreateOrg(args[0], c.User.ID)
89 orgCreateMu.Unlock()
90 if err != nil {
91 return c.fail(protocol.ExitFailure, "%v", err)
92 }
93 return c.emit(map[string]string{"org": args[0], "role": "admin"}, func(w io.Writer) {
94 fmt.Fprintf(w, "created organization %s; you are its admin\n", args[0])
95 })
96}
97
98func runOrgList(c *Ctx, args []string) int {
99 orgs, err := c.Store.ListOrgsForUser(c.User.ID)
100 if err != nil {
101 return c.fail(protocol.ExitFailure, "%v", err)
102 }
103 type out struct {
104 Org string `json:"org"`
105 Role string `json:"role"`
106 }
107 var ds []out
108 for _, o := range orgs {
109 ds = append(ds, out{o.Username, o.Role})
110 }
111 return c.emitView(ds, func(w io.Writer) {
112 tb := c.table(w, "ORG", "ROLE")
113 for _, d := range ds {
114 tb.row(cRef(d.Org), cState(d.Role))
115 }
116 tb.flush()
117 }, func() screen {
118 rows := make([]row, len(ds))
119 for i, d := range ds {
120 rows[i] = rowOf(cLink(d.Org, c.siteURL(d.Org)), cState(d.Role))
121 }
122 s := listScreen("Organizations", rows)
123 if len(ds) > 0 {
124 s.actions = []action{{"Read", []string{"org", "show", ds[0].Org}}}
125 }
126 return s
127 })
128}
129
130func runOrgShow(c *Ctx, args []string) int {
131 if len(args) != 1 {
132 return c.usage()
133 }
134 org, err := c.Store.OrgByName(args[0])
135 if errors.Is(err, store.ErrNotFound) {
136 return c.fail(protocol.ExitNotFound, "no organization %q", args[0])
137 }
138 if err != nil {
139 return c.fail(protocol.ExitFailure, "%v", err)
140 }
141 members, err := c.Store.OrgMembers(org.ID)
142 if err != nil {
143 return c.fail(protocol.ExitFailure, "%v", err)
144 }
145 type memberOut struct {
146 User string `json:"user"`
147 Role string `json:"role"`
148 }
149 var ms []memberOut
150 for _, m := range members {
151 ms = append(ms, memberOut{m.Username, m.Role})
152 }
153 d := struct {
154 Org string `json:"org"`
155 Members []memberOut `json:"members"`
156 }{org.Name, ms}
157 return c.emitView(d, func(w io.Writer) {
158 v := c.view(w)
159 v.title(d.Org, "", "")
160 if len(ms) > 0 {
161 v.section("members")
162 tb := c.table(w, "USER", "ROLE")
163 for _, m := range ms {
164 tb.row(cRef(m.User), cState(m.Role))
165 }
166 tb.flush()
167 }
168 }, func() screen {
169 members := section{title: "Members", n: len(ms)}
170 for _, m := range ms {
171 members.rows = append(members.rows, rowOf(cRef(m.User), cState(m.Role)))
172 }
173 return screen{
174 fields: []field{{"Org", []cell{cLink(d.Org, c.siteURL(d.Org))}}},
175 sections: []section{members},
176 actions: []action{
177 {"Members", []string{"org", "members", "add", d.Org, "<user>"}},
178 {"Org", []string{"org", "team", "list", d.Org}},
179 {"Org", []string{"org", "label", "list", d.Org}},
180 {"Org", []string{"org", "milestone", "list", d.Org}},
181 },
182 }
183 })
184}
185
186func runOrgRename(c *Ctx, args []string) int {
187 if len(args) != 2 {
188 return c.usage()
189 }
190 org, code := orgAdmin(c, args[0])
191 if code >= 0 {
192 return code
193 }
194 newName := args[1]
195 if err := policy.ValidateOwnerName(newName); err != nil {
196 return c.failInput(err)
197 }
198 oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name)
199 newDir := filepath.Join(c.Cfg.Server.Root, "repos", newName)
200 if _, err := os.Stat(newDir); err == nil {
201 return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName)
202 }
203 release, lockCode := holdOffBackup(c)
204 if lockCode >= 0 {
205 return lockCode
206 }
207 defer release()
208 if err := c.Store.RenameOrg(org.ID, newName); err != nil {
209 return c.failErr(err)
210 }
211 // Repo paths on disk derive from the owner name; move the tree. If the
212 // move fails, revert the database so name and disk stay consistent.
213 if _, err := os.Stat(oldDir); err == nil {
214 if err := os.Rename(oldDir, newDir); err != nil {
215 c.Store.RenameOrg(org.ID, org.Name)
216 return c.fail(protocol.ExitFailure, "moving repositories: %v", err)
217 }
218 }
219 return c.emit(map[string]string{"org": newName, "was": org.Name}, func(w io.Writer) {
220 fmt.Fprintf(w, "renamed %s to %s — clone URLs now use %s/<repo>\n", org.Name, newName, newName)
221 })
222}
223
224func runOrgDelete(c *Ctx, args []string) int {
225 var name string
226 yes := false
227 for _, a := range args {
228 if a == "--yes" {
229 yes = true
230 } else if name == "" {
231 name = a
232 } else {
233 return c.usage()
234 }
235 }
236 if name == "" {
237 return c.usage()
238 }
239 org, code := orgAdmin(c, name)
240 if code >= 0 {
241 return code
242 }
243 if !yes {
244 return c.fail(protocol.ExitUsage, "org delete is permanent; re-run with --yes")
245 }
246 if err := c.Store.DeleteOrg(org.ID); err != nil {
247 return c.fail(protocol.ExitFailure, "%v", err)
248 }
249 return c.emit(map[string]string{"deleted": name}, func(w io.Writer) {
250 fmt.Fprintf(w, "deleted organization %s\n", name)
251 })
252}
253
254func runOrgMembersAdd(c *Ctx, args []string) int {
255 f, err := c.parseArgs(args, flagSpec{Values: []string{"--role"}, MaxPos: -1, Usage: "org members add <org> <user> [--role member|admin]"})
256 if err != nil {
257 return c.fail(protocol.ExitUsage, "%v", err)
258 }
259 role, rest := "member", f.Pos
260 if f.Has("--role") {
261 role = f.Value("--role")
262 }
263 if len(rest) != 2 || (role != "member" && role != "admin") {
264 return c.usage()
265 }
266 org, code := orgAdmin(c, rest[0])
267 if code >= 0 {
268 return code
269 }
270 target, err := c.Store.UserByUsername(rest[1])
271 if errors.Is(err, store.ErrNotFound) {
272 return c.fail(protocol.ExitNotFound, "no such user %q", rest[1])
273 }
274 if err != nil {
275 return c.fail(protocol.ExitFailure, "%v", err)
276 }
277 if target.Ghost {
278 return c.fail(protocol.ExitDenied, "%v", errGhost)
279 }
280 if err := c.Store.SetOrgMember(org.ID, target.ID, role); err != nil {
281 return c.failErr(err)
282 }
283 return c.emit(map[string]string{"org": org.Name, "user": target.Username, "role": role}, func(w io.Writer) {
284 fmt.Fprintf(w, "%s is now a %s of %s\n", target.Username, role, org.Name)
285 })
286}
287
288func runOrgMembersRemove(c *Ctx, args []string) int {
289 if len(args) != 2 {
290 return c.usage()
291 }
292 org, code := orgAdmin(c, args[0])
293 if code >= 0 {
294 return code
295 }
296 target, err := c.Store.UserByUsername(args[1])
297 if errors.Is(err, store.ErrNotFound) {
298 return c.fail(protocol.ExitNotFound, "no such user %q", args[1])
299 }
300 if err != nil {
301 return c.fail(protocol.ExitFailure, "%v", err)
302 }
303 if err := c.Store.RemoveOrgMember(org.ID, target.ID); err != nil {
304 if errors.Is(err, store.ErrNotFound) {
305 return c.fail(protocol.ExitNotFound, "%s is not a member of %s", target.Username, org.Name)
306 }
307 return c.failErr(err)
308 }
309 return c.emit(map[string]string{"org": org.Name, "removed": target.Username}, func(w io.Writer) {
310 fmt.Fprintf(w, "removed %s from %s\n", target.Username, org.Name)
311 })
312}
313
314func runOrgMembersList(c *Ctx, args []string) int {
315 if len(args) != 1 {
316 return c.usage()
317 }
318 return runOrgShow(c, args)
319}