sqlite.go

  1package sqlite
  2
  3import (
  4	"context"
  5	"strings"
  6
  7	"github.com/charmbracelet/soft-serve/server/access"
  8	"github.com/charmbracelet/soft-serve/server/auth"
  9	"github.com/charmbracelet/soft-serve/server/db"
 10	"github.com/charmbracelet/soft-serve/server/db/sqlite"
 11	"github.com/charmbracelet/soft-serve/server/settings"
 12	"github.com/charmbracelet/soft-serve/server/store"
 13	"github.com/charmbracelet/soft-serve/server/utils"
 14	"github.com/jmoiron/sqlx"
 15)
 16
 17// SqliteAccess is an access backend implementation that uses SQLite.
 18type SqliteAccess struct {
 19	ctx context.Context
 20	db  db.Database
 21}
 22
 23var _ access.Access = (*SqliteAccess)(nil)
 24
 25func init() {
 26	access.Register("sqlite", newSqliteAccess)
 27}
 28
 29func newSqliteAccess(ctx context.Context) (access.Access, error) {
 30	sdb := db.FromContext(ctx)
 31	if sdb == nil {
 32		return nil, db.ErrNoDatabase
 33	}
 34
 35	return &SqliteAccess{
 36		ctx: ctx,
 37		db:  sdb,
 38	}, nil
 39}
 40
 41// AccessLevel implements access.Access.
 42func (d *SqliteAccess) AccessLevel(ctx context.Context, repo string, user auth.User) (access.AccessLevel, error) {
 43	settings := settings.FromContext(d.ctx)
 44	store := store.FromContext(d.ctx)
 45
 46	anon := settings.AnonAccess(ctx)
 47
 48	// TODO: add admin access to user repositories
 49
 50	// If the user is an admin, they have admin access.
 51	if user != nil && user.IsAdmin() {
 52		return access.AdminAccess, nil
 53	}
 54
 55	// If the repository exists, check if the user is a collaborator.
 56	r, _ := store.Repository(ctx, repo)
 57	if r != nil {
 58		// If the user is a collaborator, they have read/write access.
 59		if user != nil {
 60			isCollab, _ := d.IsCollaborator(ctx, repo, user.Username())
 61			if isCollab {
 62				if anon > access.ReadWriteAccess {
 63					return anon, nil
 64				}
 65				return access.ReadWriteAccess, nil
 66			}
 67		}
 68
 69		// If the repository is private, the user has no access.
 70		if r.IsPrivate() {
 71			return access.NoAccess, nil
 72		}
 73
 74		// Otherwise, the user has read-only access.
 75		return access.ReadOnlyAccess, nil
 76	}
 77
 78	if user != nil {
 79		// If the repository doesn't exist, the user has read/write access.
 80		if anon > access.ReadWriteAccess {
 81			return anon, nil
 82		}
 83
 84		return access.ReadWriteAccess, nil
 85	}
 86
 87	// If the user doesn't exist, give them the anonymous access level.
 88	return anon, nil
 89}
 90
 91// AddCollaborator adds a collaborator to a repository.
 92//
 93// It implements backend.Backend.
 94func (d *SqliteAccess) AddCollaborator(ctx context.Context, repo string, username string) error {
 95	username = strings.ToLower(username)
 96	if err := utils.ValidateUsername(username); err != nil {
 97		return err
 98	}
 99
100	repo = utils.SanitizeRepo(repo)
101	return sqlite.WrapDbErr(sqlite.WrapTx(d.db.DBx(), ctx, func(tx *sqlx.Tx) error {
102		_, err := tx.Exec(`INSERT INTO collab (user_id, repo_id, updated_at)
103			VALUES (
104			(SELECT id FROM user WHERE username = ?),
105			(SELECT id FROM repo WHERE name = ?),
106			CURRENT_TIMESTAMP
107			);`, username, repo)
108		return err
109	}),
110	)
111}
112
113// Collaborators returns a list of collaborators for a repository.
114//
115// It implements backend.Backend.
116func (d *SqliteAccess) Collaborators(ctx context.Context, repo string) ([]string, error) {
117	repo = utils.SanitizeRepo(repo)
118	var users []string
119	if err := sqlite.WrapTx(d.db.DBx(), d.ctx, func(tx *sqlx.Tx) error {
120		return tx.Select(&users, `SELECT user.username FROM user
121			INNER JOIN collab ON user.id = collab.user_id
122			INNER JOIN repo ON repo.id = collab.repo_id
123			WHERE repo.name = ?`, repo)
124	}); err != nil {
125		return nil, sqlite.WrapDbErr(err)
126	}
127
128	return users, nil
129}
130
131// IsCollaborator returns true if the user is a collaborator of the repository.
132//
133// It implements backend.Backend.
134func (d *SqliteAccess) IsCollaborator(ctx context.Context, repo string, username string) (bool, error) {
135	repo = utils.SanitizeRepo(repo)
136	var count int
137	if err := sqlite.WrapTx(d.db.DBx(), ctx, func(tx *sqlx.Tx) error {
138		return tx.Get(&count, `SELECT COUNT(*) FROM user
139			INNER JOIN collab ON user.id = collab.user_id
140			INNER JOIN repo ON repo.id = collab.repo_id
141			WHERE repo.name = ? AND user.username = ?`, repo, username)
142	}); err != nil {
143		return false, sqlite.WrapDbErr(err)
144	}
145
146	return count > 0, nil
147}
148
149// RemoveCollaborator removes a collaborator from a repository.
150//
151// It implements backend.Backend.
152func (d *SqliteAccess) RemoveCollaborator(ctx context.Context, repo string, username string) error {
153	repo = utils.SanitizeRepo(repo)
154	return sqlite.WrapDbErr(
155		sqlite.WrapTx(d.db.DBx(), ctx, func(tx *sqlx.Tx) error {
156			_, err := tx.Exec(`DELETE FROM collab
157			WHERE user_id = (SELECT id FROM user WHERE username = ?)
158			AND repo_id = (SELECT id FROM repo WHERE name = ?)`, username, repo)
159			return err
160		}),
161	)
162}