diff --git a/cmd/soft/migrate_config.go b/cmd/soft/migrate_config.go index 583153e5d84e1bbd3e73538267ed6bf85f0d36cb..86bea23d39ad148efb7b5a0cea1f2d2663306fb4 100644 --- a/cmd/soft/migrate_config.go +++ b/cmd/soft/migrate_config.go @@ -273,7 +273,7 @@ var migrateConfig = &cobra.Command{ } for _, collab := range r.Collabs { - if err := sb.AddCollaborator(ctx, repo, collab); err != nil { + if err := sb.AddCollaborator(ctx, repo, collab, access.ReadWriteAccess); err != nil { logger.Errorf("failed to add repo collab to %s: %s", repo, err) } } @@ -308,7 +308,7 @@ var migrateConfig = &cobra.Command{ } for _, repo := range user.CollabRepos { - if err := sb.AddCollaborator(ctx, repo, username); err != nil { + if err := sb.AddCollaborator(ctx, repo, username, access.ReadWriteAccess); err != nil { logger.Errorf("failed to add user collab to %s: %s\n", repo, err) } } diff --git a/server/access/access.go b/server/access/access.go index 2ddc88b398c8e00eafeaee91e328859ccaaa38ba..44ec3828e15186c1b406ce150b2297afb6f59836 100644 --- a/server/access/access.go +++ b/server/access/access.go @@ -1,5 +1,10 @@ package access +import ( + "encoding" + "errors" +) + // AccessLevel is the level of access allowed to a repo. type AccessLevel int // nolint: revive @@ -48,3 +53,26 @@ func ParseAccessLevel(s string) AccessLevel { return AccessLevel(-1) } } + +var _ encoding.TextMarshaler = AccessLevel(0) +var _ encoding.TextUnmarshaler = (*AccessLevel)(nil) + +// ErrInvalidAccessLevel is returned when an invalid access level is provided. +var ErrInvalidAccessLevel = errors.New("invalid access level") + +// UnmarshalText implements encoding.TextUnmarshaler. +func (a *AccessLevel) UnmarshalText(text []byte) error { + l := ParseAccessLevel(string(text)) + if l < 0 { + return ErrInvalidAccessLevel + } + + *a = l + + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (a AccessLevel) MarshalText() (text []byte, err error) { + return []byte(a.String()), nil +} diff --git a/server/backend/collab.go b/server/backend/collab.go index 78c5b8a230ab285875953e62aeb273d69eef0381..ffe4b5f6ee725cc6f0b47d7e68b9f7464b674e46 100644 --- a/server/backend/collab.go +++ b/server/backend/collab.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/charmbracelet/soft-serve/server/access" "github.com/charmbracelet/soft-serve/server/db" "github.com/charmbracelet/soft-serve/server/db/models" "github.com/charmbracelet/soft-serve/server/utils" @@ -12,7 +13,7 @@ import ( // AddCollaborator adds a collaborator to a repository. // // It implements backend.Backend. -func (d *Backend) AddCollaborator(ctx context.Context, repo string, username string) error { +func (d *Backend) AddCollaborator(ctx context.Context, repo string, username string, level access.AccessLevel) error { username = strings.ToLower(username) if err := utils.ValidateUsername(username); err != nil { return err @@ -21,7 +22,7 @@ func (d *Backend) AddCollaborator(ctx context.Context, repo string, username str repo = utils.SanitizeRepo(repo) return db.WrapError( d.db.TransactionContext(ctx, func(tx *db.Tx) error { - return d.store.AddCollabByUsernameAndRepo(ctx, tx, username, repo) + return d.store.AddCollabByUsernameAndRepo(ctx, tx, username, repo, level) }), ) } @@ -48,12 +49,12 @@ func (d *Backend) Collaborators(ctx context.Context, repo string) ([]string, err return usernames, nil } -// IsCollaborator returns true if the user is a collaborator of the repository. +// IsCollaborator returns the access level and true if the user is a collaborator of the repository. // // It implements backend.Backend. -func (d *Backend) IsCollaborator(ctx context.Context, repo string, username string) (bool, error) { +func (d *Backend) IsCollaborator(ctx context.Context, repo string, username string) (access.AccessLevel, bool, error) { if username == "" { - return false, nil + return -1, false, nil } repo = utils.SanitizeRepo(repo) @@ -63,10 +64,10 @@ func (d *Backend) IsCollaborator(ctx context.Context, repo string, username stri m, err = d.store.GetCollabByUsernameAndRepo(ctx, tx, username, repo) return err }); err != nil { - return false, db.WrapError(err) + return -1, false, db.WrapError(err) } - return m.ID > 0, nil + return m.AccessLevel, m.ID > 0, nil } // RemoveCollaborator removes a collaborator from a repository. diff --git a/server/backend/user.go b/server/backend/user.go index 07db904d55d26d8d0cbc28407d0cb4de7960298c..d5cf38ca329af86bc2e4487c66abf5990e644aa9 100644 --- a/server/backend/user.go +++ b/server/backend/user.go @@ -69,13 +69,13 @@ func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user prot } } - // If the user is a collaborator, they have read/write access. - isCollab, _ := d.IsCollaborator(ctx, repo, username) + // If the user is a collaborator, they have return their access level. + collabAccess, isCollab, _ := d.IsCollaborator(ctx, repo, username) if isCollab { - if anon > access.ReadWriteAccess { + if anon > collabAccess { return anon } - return access.ReadWriteAccess + return collabAccess } // If the repository is private, the user has no access. diff --git a/server/db/migrate/0001_create_tables.go b/server/db/migrate/0001_create_tables.go index 6df595cb29583a83667b809089cfb1c58c7114be..d1cadd4d4bf3e517e8912d76cb1997c7de2b4430 100644 --- a/server/db/migrate/0001_create_tables.go +++ b/server/db/migrate/0001_create_tables.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/charmbracelet/soft-serve/server/access" "github.com/charmbracelet/soft-serve/server/config" @@ -118,8 +119,8 @@ var createTables = Migration{ if hasTable(tx, "collab_old") { sqlm := ` - INSERT INTO collabs (id, user_id, repo_id, created_at, updated_at) - SELECT id, user_id, repo_id, created_at, updated_at FROM collab_old; + INSERT INTO collabs (id, user_id, repo_id, access_level, created_at, updated_at) + SELECT id, user_id, repo_id, ` + strconv.Itoa(int(access.ReadWriteAccess)) + `, created_at, updated_at FROM collab_old; ` if _, err := tx.ExecContext(ctx, sqlm); err != nil { return err diff --git a/server/db/migrate/0001_create_tables_postgres.up.sql b/server/db/migrate/0001_create_tables_postgres.up.sql index 59cf16a58e550ddcab0066203f3166d0fa01175c..7e2d9275ded141f9cbbaaf95d7859b62358eb082 100644 --- a/server/db/migrate/0001_create_tables_postgres.up.sql +++ b/server/db/migrate/0001_create_tables_postgres.up.sql @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS collabs ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, repo_id INTEGER NOT NULL, + access_level INTEGER NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL, UNIQUE (user_id, repo_id), diff --git a/server/db/migrate/0001_create_tables_sqlite.up.sql b/server/db/migrate/0001_create_tables_sqlite.up.sql index dad8d3f05dc281f8db440479250749d3fabf042e..9a1b15fb192b6373f6330c002661d38a601703cb 100644 --- a/server/db/migrate/0001_create_tables_sqlite.up.sql +++ b/server/db/migrate/0001_create_tables_sqlite.up.sql @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS collabs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, repo_id INTEGER NOT NULL, + access_level INTEGER NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL, UNIQUE (user_id, repo_id), diff --git a/server/db/models/collab.go b/server/db/models/collab.go index e14660189d85fb325ca348492fb02216a6906780..7efc44b9d3b7350b1b14c4f0640948447c8f28df 100644 --- a/server/db/models/collab.go +++ b/server/db/models/collab.go @@ -1,12 +1,17 @@ package models -import "time" +import ( + "time" + + "github.com/charmbracelet/soft-serve/server/access" +) // Collab represents a repository collaborator. type Collab struct { - ID int64 `db:"id"` - RepoID int64 `db:"repo_id"` - UserID int64 `db:"user_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` + ID int64 `db:"id"` + RepoID int64 `db:"repo_id"` + UserID int64 `db:"user_id"` + AccessLevel access.AccessLevel `db:"access_level"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } diff --git a/server/ssh/cmd/collab.go b/server/ssh/cmd/collab.go index 92a0829d8b4dc5ba0db8449233db2825b5e60aeb..7f45939a9a42a1d1262bc91c0f39eabbdeade1e8 100644 --- a/server/ssh/cmd/collab.go +++ b/server/ssh/cmd/collab.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/charmbracelet/soft-serve/server/access" "github.com/charmbracelet/soft-serve/server/backend" "github.com/spf13/cobra" ) @@ -23,17 +24,25 @@ func collabCommand() *cobra.Command { func collabAddCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "add REPOSITORY USERNAME", + Use: "add REPOSITORY USERNAME [LEVEL]", Short: "Add a collaborator to a repo", - Args: cobra.ExactArgs(2), + 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.", + Args: cobra.RangeArgs(2, 3), PersistentPreRunE: checkIfCollab, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() be := backend.FromContext(ctx) repo := args[0] username := args[1] + level := access.ReadWriteAccess + if len(args) > 2 { + level = access.ParseAccessLevel(args[2]) + if level < 0 { + return access.ErrInvalidAccessLevel + } + } - return be.AddCollaborator(ctx, repo, username) + return be.AddCollaborator(ctx, repo, username, level) }, } diff --git a/server/store/collab.go b/server/store/collab.go index 430e39d8f667dfb82a57bb53b29d6656b2d95eb6..f8907e1ca8487204ee37939cbbda5382dc92ddf3 100644 --- a/server/store/collab.go +++ b/server/store/collab.go @@ -3,6 +3,7 @@ package store import ( "context" + "github.com/charmbracelet/soft-serve/server/access" "github.com/charmbracelet/soft-serve/server/db" "github.com/charmbracelet/soft-serve/server/db/models" ) @@ -10,7 +11,7 @@ import ( // CollaboratorStore is an interface for managing collaborators. type CollaboratorStore interface { GetCollabByUsernameAndRepo(ctx context.Context, h db.Handler, username string, repo string) (models.Collab, error) - AddCollabByUsernameAndRepo(ctx context.Context, h db.Handler, username string, repo string) error + AddCollabByUsernameAndRepo(ctx context.Context, h db.Handler, username string, repo string, level access.AccessLevel) error RemoveCollabByUsernameAndRepo(ctx context.Context, h db.Handler, username string, repo string) error ListCollabsByRepo(ctx context.Context, h db.Handler, repo string) ([]models.Collab, error) ListCollabsByRepoAsUsers(ctx context.Context, h db.Handler, repo string) ([]models.User, error) diff --git a/server/store/database/collab.go b/server/store/database/collab.go index e290593c2d6f5dbef4461daf147a72b9eaafb924..e93044e01cab1e49c9d57939b110001c7b64e9ba 100644 --- a/server/store/database/collab.go +++ b/server/store/database/collab.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "github.com/charmbracelet/soft-serve/server/access" "github.com/charmbracelet/soft-serve/server/db" "github.com/charmbracelet/soft-serve/server/db/models" "github.com/charmbracelet/soft-serve/server/store" @@ -15,7 +16,7 @@ type collabStore struct{} var _ store.CollaboratorStore = (*collabStore)(nil) // AddCollabByUsernameAndRepo implements store.CollaboratorStore. -func (*collabStore) AddCollabByUsernameAndRepo(ctx context.Context, tx db.Handler, username string, repo string) error { +func (*collabStore) AddCollabByUsernameAndRepo(ctx context.Context, tx db.Handler, username string, repo string, level access.AccessLevel) error { username = strings.ToLower(username) if err := utils.ValidateUsername(username); err != nil { return err @@ -23,8 +24,9 @@ func (*collabStore) AddCollabByUsernameAndRepo(ctx context.Context, tx db.Handle repo = utils.SanitizeRepo(repo) - query := tx.Rebind(`INSERT INTO collabs (user_id, repo_id, updated_at) + query := tx.Rebind(`INSERT INTO collabs (access_level, user_id, repo_id, updated_at) VALUES ( + ?, ( SELECT id FROM users WHERE username = ? ), @@ -33,7 +35,7 @@ func (*collabStore) AddCollabByUsernameAndRepo(ctx context.Context, tx db.Handle ), CURRENT_TIMESTAMP );`) - _, err := tx.ExecContext(ctx, query, username, repo) + _, err := tx.ExecContext(ctx, query, level, username, repo) return err } diff --git a/testscript/testdata/repo-perms.txtar b/testscript/testdata/repo-perms.txtar index cf21151b55dde6e58e7853172aaad422bbb9e775..66341b9fecea85d043d3722bcc11aa89c2189fca 100644 --- a/testscript/testdata/repo-perms.txtar +++ b/testscript/testdata/repo-perms.txtar @@ -60,7 +60,9 @@ stderr 'unauthorized' stderr 'unauthorized' # add user1 as collab -soft repo collab add repo1 user1 +! soft repo collab add repo1 user1 foobar +stderr 'invalid access level' +soft repo collab add repo1 user1 read-write soft repo collab list repo1 stdout user1 usoft repo collab list repo1