internal/control/org.go

319 lines · 9549 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		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}