Skip to content

Commit

Permalink
feat: add collaborators with access level
Browse files Browse the repository at this point in the history
Now you can add a collaborator with a specific access level

Fixes: #281
  • Loading branch information
aymanbagabas committed Aug 4, 2023
1 parent c4dde1c commit 882e701
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 29 deletions.
4 changes: 2 additions & 2 deletions cmd/soft/migrate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
}
}
Expand Down
28 changes: 28 additions & 0 deletions server/access/access.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package access

import (
"encoding"
"errors"
)

// AccessLevel is the level of access allowed to a repo.
type AccessLevel int // nolint: revive

Expand Down Expand Up @@ -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
}
15 changes: 8 additions & 7 deletions server/backend/collab.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)
}),
)
}
Expand All @@ -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)
Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions server/backend/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions server/db/migrate/0001_create_tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"

"github.com/charmbracelet/soft-serve/server/access"
"github.com/charmbracelet/soft-serve/server/config"
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/db/migrate/0001_create_tables_postgres.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions server/db/migrate/0001_create_tables_sqlite.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
17 changes: 11 additions & 6 deletions server/db/models/collab.go
Original file line number Diff line number Diff line change
@@ -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"`
}
15 changes: 12 additions & 3 deletions server/ssh/cmd/collab.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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)
},
}

Expand Down
3 changes: 2 additions & 1 deletion server/store/collab.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ 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"
)

// 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)
Expand Down
8 changes: 5 additions & 3 deletions server/store/database/collab.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -15,16 +16,17 @@ 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
}

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 = ?
),
Expand All @@ -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
}

Expand Down
4 changes: 3 additions & 1 deletion testscript/testdata/repo-perms.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 882e701

Please sign in to comment.