internal/control/org.go

5e9732fb1d5142142fdae2985f07125cf852647c
gitbay/internal/control/org.go history · blame · raw

256 lines · 8072 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", name)
 57	}
 58	return org, -1
 59}
 60
 61func runOrgCreate(c *Ctx, args []string) int {
 62	if len(args) != 1 {
 63		return c.fail(protocol.ExitUsage, "usage: org create <name>")
 64	}
 65	if err := policy.ValidateOwnerName(args[0]); err != nil {
 66		return c.failErr(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.fail(protocol.ExitUsage, "usage: org show <name>")
 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.fail(protocol.ExitUsage, "usage: org rename <old> <new>")
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.failErr(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.fail(protocol.ExitUsage, "usage: org delete <name> --yes")
174		}
175	}
176	if name == "" {
177		return c.fail(protocol.ExitUsage, "usage: org delete <name> --yes")
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.fail(protocol.ExitUsage, "usage: org members add <org> <user> [--role member|admin]")
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.fail(protocol.ExitUsage, "usage: org members remove <org> <user>")
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.fail(protocol.ExitUsage, "usage: org members list <org>")
254	}
255	return runOrgShow(c, args)
256}