internal/control/org.go

bd5cf5d7d1f34fa780660fd7562b9ffd9746ee27
gitbay/internal/control/org.go history · blame · raw

283 lines · 8440 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	release, lockCode := holdOffBackup(c)
171	if lockCode >= 0 {
172		return lockCode
173	}
174	defer release()
175	if err := c.Store.RenameOrg(org.ID, newName); err != nil {
176		return c.failErr(err)
177	}
178	// Repo paths on disk derive from the owner name; move the tree. If the
179	// move fails, revert the database so name and disk stay consistent.
180	if _, err := os.Stat(oldDir); err == nil {
181		if err := os.Rename(oldDir, newDir); err != nil {
182			c.Store.RenameOrg(org.ID, org.Name)
183			return c.fail(protocol.ExitFailure, "moving repositories: %v", err)
184		}
185	}
186	return c.emit(map[string]string{"org": newName, "was": org.Name}, func(w io.Writer) {
187		fmt.Fprintf(w, "renamed %s to %s — clone URLs now use %s/<repo>\n", org.Name, newName, newName)
188	})
189}
190
191func runOrgDelete(c *Ctx, args []string) int {
192	var name string
193	yes := false
194	for _, a := range args {
195		if a == "--yes" {
196			yes = true
197		} else if name == "" {
198			name = a
199		} else {
200			return c.usage()
201		}
202	}
203	if name == "" {
204		return c.usage()
205	}
206	org, code := orgAdmin(c, name)
207	if code >= 0 {
208		return code
209	}
210	if !yes {
211		return c.fail(protocol.ExitUsage, "org delete is permanent; re-run with --yes")
212	}
213	if err := c.Store.DeleteOrg(org.ID); err != nil {
214		return c.fail(protocol.ExitFailure, "%v", err)
215	}
216	return c.emit(map[string]string{"deleted": name}, func(w io.Writer) {
217		fmt.Fprintf(w, "deleted organization %s\n", name)
218	})
219}
220
221func runOrgMembersAdd(c *Ctx, args []string) int {
222	f, err := c.parseArgs(args, flagSpec{Values: []string{"--role"}, MaxPos: -1, Usage: "org members add <org> <user> [--role member|admin]"})
223	if err != nil {
224		return c.fail(protocol.ExitUsage, "%v", err)
225	}
226	role, rest := "member", f.Pos
227	if f.Has("--role") {
228		role = f.Value("--role")
229	}
230	if len(rest) != 2 || (role != "member" && role != "admin") {
231		return c.usage()
232	}
233	org, code := orgAdmin(c, rest[0])
234	if code >= 0 {
235		return code
236	}
237	target, err := c.Store.UserByUsername(rest[1])
238	if errors.Is(err, store.ErrNotFound) {
239		return c.fail(protocol.ExitNotFound, "no such user %q", rest[1])
240	}
241	if err != nil {
242		return c.fail(protocol.ExitFailure, "%v", err)
243	}
244	if err := c.Store.SetOrgMember(org.ID, target.ID, role); err != nil {
245		return c.failErr(err)
246	}
247	return c.emit(map[string]string{"org": org.Name, "user": target.Username, "role": role}, func(w io.Writer) {
248		fmt.Fprintf(w, "%s is now a %s of %s\n", target.Username, role, org.Name)
249	})
250}
251
252func runOrgMembersRemove(c *Ctx, args []string) int {
253	if len(args) != 2 {
254		return c.usage()
255	}
256	org, code := orgAdmin(c, args[0])
257	if code >= 0 {
258		return code
259	}
260	target, err := c.Store.UserByUsername(args[1])
261	if errors.Is(err, store.ErrNotFound) {
262		return c.fail(protocol.ExitNotFound, "no such user %q", args[1])
263	}
264	if err != nil {
265		return c.fail(protocol.ExitFailure, "%v", err)
266	}
267	if err := c.Store.RemoveOrgMember(org.ID, target.ID); err != nil {
268		if errors.Is(err, store.ErrNotFound) {
269			return c.fail(protocol.ExitNotFound, "%s is not a member of %s", target.Username, org.Name)
270		}
271		return c.failErr(err)
272	}
273	return c.emit(map[string]string{"org": org.Name, "removed": target.Username}, func(w io.Writer) {
274		fmt.Fprintf(w, "removed %s from %s\n", target.Username, org.Name)
275	})
276}
277
278func runOrgMembersList(c *Ctx, args []string) int {
279	if len(args) != 1 {
280		return c.usage()
281	}
282	return runOrgShow(c, args)
283}