Parent directory

collab.go

4417 bytes
  1package cmd
  2
  3import (
  4	"context"
  5	"errors"
  6
  7	"github.com/charmbracelet/soft-serve/pkg/access"
  8	"github.com/charmbracelet/soft-serve/pkg/backend"
  9	"github.com/charmbracelet/soft-serve/pkg/db"
 10	"github.com/charmbracelet/soft-serve/pkg/proto"
 11	"github.com/spf13/cobra"
 12)
 13
 14func collabCommand() *cobra.Command {
 15	cmd := &cobra.Command{
 16		Use:     "collab",
 17		Aliases: []string{"collabs", "collaborator", "collaborators"},
 18		Short:   "Manage collaborators",
 19	}
 20
 21	cmd.AddCommand(
 22		collabAddCommand(),
 23		collabRemoveCommand(),
 24		collabListCommand(),
 25	)
 26
 27	return cmd
 28}
 29
 30// checkCollabGrant reports whether the caller may set a collaborator's access
 31// level on a repository to level.
 32//
 33// Server admins may grant anything. Everyone else is bounded by their own
 34// access level on the repository, so a read-write collaborator cannot mint an
 35// admin-access collaborator and escalate beyond their own permissions.
 36func checkCollabGrant(ctx context.Context, repo string, level access.AccessLevel) error {
 37	if isServerAdmin(ctx) {
 38		return nil
 39	}
 40
 41	if proto.UserFromContext(ctx) == nil {
 42		return proto.ErrUnauthorized
 43	}
 44
 45	caller := repoAccessLevel(ctx, repo)
 46	if level > caller {
 47		return proto.ErrExceedsAccessLevel
 48	}
 49
 50	return nil
 51}
 52
 53// checkCollabDemote reports whether the caller may remove or overwrite an
 54// existing collaborator on a repository.
 55//
 56// Removal is a privileged change in the same way granting is: without this,
 57// a read-write collaborator could remove an admin-access collaborator and
 58// then re-add them at a lower level, demoting someone above them.
 59func checkCollabDemote(ctx context.Context, repo string, username string) error {
 60	if isServerAdmin(ctx) {
 61		return nil
 62	}
 63
 64	if proto.UserFromContext(ctx) == nil {
 65		return proto.ErrUnauthorized
 66	}
 67
 68	be := backend.FromContext(ctx)
 69	current, isCollab, err := be.IsCollaborator(ctx, repo, username)
 70	if err != nil {
 71		// A missing row just means the user is not a collaborator yet, which
 72		// is the common case when adding one. Any other error is real and
 73		// must fail closed.
 74		if !errors.Is(err, db.ErrRecordNotFound) {
 75			return err
 76		}
 77		return nil
 78	}
 79
 80	if !isCollab {
 81		return nil
 82	}
 83
 84	if current > repoAccessLevel(ctx, repo) {
 85		return proto.ErrExceedsAccessLevel
 86	}
 87
 88	return nil
 89}
 90
 91func collabAddCommand() *cobra.Command {
 92	cmd := &cobra.Command{
 93		Use:               "add REPOSITORY USERNAME [LEVEL]",
 94		Short:             "Add a collaborator to a repo",
 95		Long:              "Add a collaborator to a repo. LEVEL can be one of: no-access, read-only, read-write, or admin-access. Defaults to read-write.",
 96		Args:              cobra.RangeArgs(2, 3),
 97		PersistentPreRunE: checkIfReadableAndCollab,
 98		RunE: func(cmd *cobra.Command, args []string) error {
 99			ctx := cmd.Context()
100			be := backend.FromContext(ctx)
101			repo := repoArg(args)
102			username := args[1]
103			level := access.ReadWriteAccess
104			if len(args) > 2 {
105				level = access.ParseAccessLevel(args[2])
106				if level < 0 {
107					return access.ErrInvalidAccessLevel
108				}
109			}
110
111			if err := checkCollabGrant(ctx, repo, level); err != nil {
112				return err
113			}
114
115			if err := checkCollabDemote(ctx, repo, username); err != nil {
116				return err
117			}
118
119			return be.AddCollaborator(ctx, repo, username, level)
120		},
121	}
122
123	return cmd
124}
125
126func collabRemoveCommand() *cobra.Command {
127	cmd := &cobra.Command{
128		Use:               "remove REPOSITORY USERNAME",
129		Args:              cobra.ExactArgs(2),
130		Short:             "Remove a collaborator from a repo",
131		PersistentPreRunE: checkIfReadableAndCollab,
132		RunE: func(cmd *cobra.Command, args []string) error {
133			ctx := cmd.Context()
134			be := backend.FromContext(ctx)
135			repo := repoArg(args)
136			username := args[1]
137
138			if err := checkCollabDemote(ctx, repo, username); err != nil {
139				return err
140			}
141
142			return be.RemoveCollaborator(ctx, repo, username)
143		},
144	}
145
146	return cmd
147}
148
149func collabListCommand() *cobra.Command {
150	cmd := &cobra.Command{
151		Use:               "list REPOSITORY",
152		Short:             "List collaborators for a repo",
153		Args:              cobra.ExactArgs(1),
154		PersistentPreRunE: checkIfReadableAndCollab,
155		RunE: func(cmd *cobra.Command, args []string) error {
156			ctx := cmd.Context()
157			be := backend.FromContext(ctx)
158			repo := repoArg(args)
159			collabs, err := be.Collaborators(ctx, repo)
160			if err != nil {
161				return err
162			}
163
164			for _, c := range collabs {
165				cmd.Println(c)
166			}
167
168			return nil
169		},
170	}
171
172	return cmd
173}