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