Detailed changes
@@ -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)
}
}
@@ -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
+}
@@ -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.
@@ -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.
@@ -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
@@ -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),
@@ -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),
@@ -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"`
}
@@ -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)
},
}
@@ -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)
@@ -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
}
@@ -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