internal/control/org.go
278 lines · 8343 bytes
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 Summary: "add or update a member",
41 Usage: "org members add <org> <user> [--role member|admin]",
42 Flags: []Flag{
43 {"--role", "member|admin", "the member's role", "member"},
44 },
45 Examples: []string{"org members add krz cmc --role admin"}, Run: runOrgMembersAdd})
46 register(Command{Path: []string{"org", "members", "remove"},
47 Summary: "remove a member",
48 Usage: "org members remove <org> <user>",
49 Examples: []string{"org members remove krz cmc"}, Run: runOrgMembersRemove})
50 register(Command{Path: []string{"org", "members", "list"},
51 Summary: "list members",
52 Usage: "org members list <org>",
53 Examples: []string{"org members list krz"}, ReadOnly: true, Run: runOrgMembersList})
54}
55
56// orgAdmin loads an org and requires the caller to be one of its admins.
57func orgAdmin(c *Ctx, name string) (store.Org, int) {
58 org, err := c.Store.OrgByName(name)
59 if errors.Is(err, store.ErrNotFound) {
60 return org, c.fail(protocol.ExitNotFound, "no organization %q", name)
61 }
62 if err != nil {
63 return org, c.fail(protocol.ExitFailure, "%v", err)
64 }
65 role, err := c.Store.OrgRole(org.ID, c.User.ID)
66 if err != nil {
67 return org, c.fail(protocol.ExitFailure, "%v", err)
68 }
69 if role != "admin" {
70 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that; ask one", name)
71 }
72 return org, -1
73}
74
75func runOrgCreate(c *Ctx, args []string) int {
76 if len(args) != 1 {
77 return c.usage()
78 }
79 if err := policy.ValidateOwnerName(args[0]); err != nil {
80 return c.failInput(err)
81 }
82 if _, err := c.Store.CreateOrg(args[0], c.User.ID); err != nil {
83 return c.fail(protocol.ExitFailure, "%v", err)
84 }
85 return c.emit(map[string]string{"org": args[0], "role": "admin"}, func(w io.Writer) {
86 fmt.Fprintf(w, "created organization %s; you are its admin\n", args[0])
87 })
88}
89
90func runOrgList(c *Ctx, args []string) int {
91 orgs, err := c.Store.ListOrgsForUser(c.User.ID)
92 if err != nil {
93 return c.fail(protocol.ExitFailure, "%v", err)
94 }
95 type out struct {
96 Org string `json:"org"`
97 Role string `json:"role"`
98 }
99 var ds []out
100 for _, o := range orgs {
101 ds = append(ds, out{o.Username, o.Role})
102 }
103 return c.emit(ds, func(w io.Writer) {
104 tb := c.table(w, "ORG", "ROLE")
105 for _, d := range ds {
106 tb.row(cRef(d.Org), cState(d.Role))
107 }
108 tb.flush()
109 })
110}
111
112func runOrgShow(c *Ctx, args []string) int {
113 if len(args) != 1 {
114 return c.usage()
115 }
116 org, err := c.Store.OrgByName(args[0])
117 if errors.Is(err, store.ErrNotFound) {
118 return c.fail(protocol.ExitNotFound, "no organization %q", args[0])
119 }
120 if err != nil {
121 return c.fail(protocol.ExitFailure, "%v", err)
122 }
123 members, err := c.Store.OrgMembers(org.ID)
124 if err != nil {
125 return c.fail(protocol.ExitFailure, "%v", err)
126 }
127 type memberOut struct {
128 User string `json:"user"`
129 Role string `json:"role"`
130 }
131 var ms []memberOut
132 for _, m := range members {
133 ms = append(ms, memberOut{m.Username, m.Role})
134 }
135 d := struct {
136 Org string `json:"org"`
137 Members []memberOut `json:"members"`
138 }{org.Name, ms}
139 return c.emit(d, func(w io.Writer) {
140 v := c.view(w)
141 v.title(d.Org, "", "")
142 if len(ms) > 0 {
143 v.section("members")
144 tb := c.table(w, "USER", "ROLE")
145 for _, m := range ms {
146 tb.row(cRef(m.User), cState(m.Role))
147 }
148 tb.flush()
149 }
150 })
151}
152
153func runOrgRename(c *Ctx, args []string) int {
154 if len(args) != 2 {
155 return c.usage()
156 }
157 org, code := orgAdmin(c, args[0])
158 if code >= 0 {
159 return code
160 }
161 newName := args[1]
162 if err := policy.ValidateOwnerName(newName); err != nil {
163 return c.failInput(err)
164 }
165 oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name)
166 newDir := filepath.Join(c.Cfg.Server.Root, "repos", newName)
167 if _, err := os.Stat(newDir); err == nil {
168 return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName)
169 }
170 if err := c.Store.RenameOrg(org.ID, newName); err != nil {
171 return c.failErr(err)
172 }
173 // Repo paths on disk derive from the owner name; move the tree. If the
174 // move fails, revert the database so name and disk stay consistent.
175 if _, err := os.Stat(oldDir); err == nil {
176 if err := os.Rename(oldDir, newDir); err != nil {
177 c.Store.RenameOrg(org.ID, org.Name)
178 return c.fail(protocol.ExitFailure, "moving repositories: %v", err)
179 }
180 }
181 return c.emit(map[string]string{"org": newName, "was": org.Name}, func(w io.Writer) {
182 fmt.Fprintf(w, "renamed %s to %s — clone URLs now use %s/<repo>\n", org.Name, newName, newName)
183 })
184}
185
186func runOrgDelete(c *Ctx, args []string) int {
187 var name string
188 yes := false
189 for _, a := range args {
190 if a == "--yes" {
191 yes = true
192 } else if name == "" {
193 name = a
194 } else {
195 return c.usage()
196 }
197 }
198 if name == "" {
199 return c.usage()
200 }
201 org, code := orgAdmin(c, name)
202 if code >= 0 {
203 return code
204 }
205 if !yes {
206 return c.fail(protocol.ExitUsage, "org delete is permanent; re-run with --yes")
207 }
208 if err := c.Store.DeleteOrg(org.ID); err != nil {
209 return c.fail(protocol.ExitFailure, "%v", err)
210 }
211 return c.emit(map[string]string{"deleted": name}, func(w io.Writer) {
212 fmt.Fprintf(w, "deleted organization %s\n", name)
213 })
214}
215
216func runOrgMembersAdd(c *Ctx, args []string) int {
217 f, err := c.parseArgs(args, flagSpec{Values: []string{"--role"}, MaxPos: -1, Usage: "org members add <org> <user> [--role member|admin]"})
218 if err != nil {
219 return c.fail(protocol.ExitUsage, "%v", err)
220 }
221 role, rest := "member", f.Pos
222 if f.Has("--role") {
223 role = f.Value("--role")
224 }
225 if len(rest) != 2 || (role != "member" && role != "admin") {
226 return c.usage()
227 }
228 org, code := orgAdmin(c, rest[0])
229 if code >= 0 {
230 return code
231 }
232 target, err := c.Store.UserByUsername(rest[1])
233 if errors.Is(err, store.ErrNotFound) {
234 return c.fail(protocol.ExitNotFound, "no such user %q", rest[1])
235 }
236 if err != nil {
237 return c.fail(protocol.ExitFailure, "%v", err)
238 }
239 if err := c.Store.SetOrgMember(org.ID, target.ID, role); err != nil {
240 return c.failErr(err)
241 }
242 return c.emit(map[string]string{"org": org.Name, "user": target.Username, "role": role}, func(w io.Writer) {
243 fmt.Fprintf(w, "%s is now a %s of %s\n", target.Username, role, org.Name)
244 })
245}
246
247func runOrgMembersRemove(c *Ctx, args []string) int {
248 if len(args) != 2 {
249 return c.usage()
250 }
251 org, code := orgAdmin(c, args[0])
252 if code >= 0 {
253 return code
254 }
255 target, err := c.Store.UserByUsername(args[1])
256 if errors.Is(err, store.ErrNotFound) {
257 return c.fail(protocol.ExitNotFound, "no such user %q", args[1])
258 }
259 if err != nil {
260 return c.fail(protocol.ExitFailure, "%v", err)
261 }
262 if err := c.Store.RemoveOrgMember(org.ID, target.ID); err != nil {
263 if errors.Is(err, store.ErrNotFound) {
264 return c.fail(protocol.ExitNotFound, "%s is not a member of %s", target.Username, org.Name)
265 }
266 return c.failErr(err)
267 }
268 return c.emit(map[string]string{"org": org.Name, "removed": target.Username}, func(w io.Writer) {
269 fmt.Fprintf(w, "removed %s from %s\n", target.Username, org.Name)
270 })
271}
272
273func runOrgMembersList(c *Ctx, args []string) int {
274 if len(args) != 1 {
275 return c.usage()
276 }
277 return runOrgShow(c, args)
278}