1package backend23import (4 "context"5 "errors"6 "strings"7 "time"89 "github.com/charmbracelet/soft-serve/pkg/access"10 "github.com/charmbracelet/soft-serve/pkg/db"11 "github.com/charmbracelet/soft-serve/pkg/db/models"12 "github.com/charmbracelet/soft-serve/pkg/proto"13 "github.com/charmbracelet/soft-serve/pkg/sshutils"14 "github.com/charmbracelet/soft-serve/pkg/utils"15 "golang.org/x/crypto/ssh"16)1718// AccessLevel returns the access level of a user for a repository.19//20// It implements backend.Backend.21func (d *Backend) AccessLevel(ctx context.Context, repo string, username string) access.AccessLevel {22 user, _ := d.User(ctx, username)23 return d.AccessLevelForUser(ctx, repo, user)24}2526// AccessLevelByPublicKey returns the access level of a user's public key for a repository.27//28// It implements backend.Backend.29func (d *Backend) AccessLevelByPublicKey(ctx context.Context, repo string, pk ssh.PublicKey) access.AccessLevel {30 for _, k := range d.cfg.AdminKeys() {31 if sshutils.KeysEqual(pk, k) {32 return access.AdminAccess33 }34 }3536 user, _ := d.UserByPublicKey(ctx, pk)37 if user != nil {38 return d.AccessLevel(ctx, repo, user.Username())39 }4041 return d.AccessLevel(ctx, repo, "")42}4344// AccessLevelForUser returns the access level of a user for a repository.45// TODO: user repository ownership46func (d *Backend) AccessLevelForUser(ctx context.Context, repo string, user proto.User) access.AccessLevel {47 var username string48 anon := d.AnonAccess(ctx)49 if user != nil {50 username = user.Username()51 }5253 // If the user is an admin, they have admin access.54 if user != nil && user.IsAdmin() {55 return access.AdminAccess56 }5758 // If the repository exists, check if the user is a collaborator.59 r := proto.RepositoryFromContext(ctx)60 if r == nil {61 r, _ = d.Repository(ctx, repo)62 }6364 if r != nil {65 if user != nil {66 // If the user is the owner, they have admin access.67 if r.UserID() == user.ID() {68 return access.AdminAccess69 }70 }7172 // If the user is a collaborator, they have return their access level.73 collabAccess, isCollab, _ := d.IsCollaborator(ctx, repo, username)74 if isCollab {75 if anon > collabAccess {76 return anon77 }78 return collabAccess79 }8081 // If the repository is private, the user has no access.82 if r.IsPrivate() {83 return access.NoAccess84 }8586 // Otherwise, the user has read-only access.87 if user == nil {88 return anon89 }9091 return access.ReadOnlyAccess92 }9394 if user != nil {95 // If the repository doesn't exist, the user has read/write access.96 if anon > access.ReadWriteAccess {97 return anon98 }99100 return access.ReadWriteAccess101 }102103 // If the user doesn't exist, give them the anonymous access level.104 return anon105}106107// User finds a user by username.108//109// It implements backend.Backend.110func (d *Backend) User(ctx context.Context, username string) (proto.User, error) {111 username = strings.ToLower(username)112 if err := utils.ValidateUsername(username); err != nil {113 return nil, err114 }115116 var m models.User117 var pks []ssh.PublicKey118 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {119 var err error120 m, err = d.store.FindUserByUsername(ctx, tx, username)121 if err != nil {122 return err123 }124125 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)126 return err127 }); err != nil {128 err = db.WrapError(err)129 if errors.Is(err, db.ErrRecordNotFound) {130 return nil, proto.ErrUserNotFound131 }132 d.logger.Error("error finding user", "username", username, "error", err)133 return nil, err134 }135136 return &user{137 user: m,138 publicKeys: pks,139 }, nil140}141142// UserByID finds a user by ID.143func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {144 var m models.User145 var pks []ssh.PublicKey146 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {147 var err error148 m, err = d.store.GetUserByID(ctx, tx, id)149 if err != nil {150 return err151 }152153 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)154 return err155 }); err != nil {156 err = db.WrapError(err)157 if errors.Is(err, db.ErrRecordNotFound) {158 return nil, proto.ErrUserNotFound159 }160 d.logger.Error("error finding user", "id", id, "error", err)161 return nil, err162 }163164 return &user{165 user: m,166 publicKeys: pks,167 }, nil168}169170// UserByPublicKey finds a user by public key.171//172// It implements backend.Backend.173func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.User, error) {174 var m models.User175 var pks []ssh.PublicKey176 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {177 var err error178 m, err = d.store.FindUserByPublicKey(ctx, tx, pk)179 if err != nil {180 return db.WrapError(err)181 }182183 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)184 return err185 }); err != nil {186 err = db.WrapError(err)187 if errors.Is(err, db.ErrRecordNotFound) {188 return nil, proto.ErrUserNotFound189 }190 d.logger.Error("error finding user", "pk", sshutils.MarshalAuthorizedKey(pk), "error", err)191 return nil, err192 }193194 return &user{195 user: m,196 publicKeys: pks,197 }, nil198}199200// UserByAccessToken finds a user by access token.201// This also validates the token for expiration and returns proto.ErrTokenExpired.202func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.User, error) {203 var m models.User204 var pks []ssh.PublicKey205 token = HashToken(token)206207 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {208 t, err := d.store.GetAccessTokenByToken(ctx, tx, token)209 if err != nil {210 return db.WrapError(err)211 }212213 if t.ExpiresAt.Valid && t.ExpiresAt.Time.Before(time.Now()) {214 return proto.ErrTokenExpired215 }216217 m, err = d.store.FindUserByAccessToken(ctx, tx, token)218 if err != nil {219 return db.WrapError(err)220 }221222 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)223 return err224 }); err != nil {225 err = db.WrapError(err)226 if errors.Is(err, db.ErrRecordNotFound) {227 return nil, proto.ErrUserNotFound228 }229 d.logger.Error("failed to find user by access token", "err", err, "token", token)230 return nil, err231 }232233 return &user{234 user: m,235 publicKeys: pks,236 }, nil237}238239// Users returns all users.240//241// It implements backend.Backend.242func (d *Backend) Users(ctx context.Context) ([]string, error) {243 var users []string244 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {245 ms, err := d.store.GetAllUsers(ctx, tx)246 if err != nil {247 return err248 }249250 for _, m := range ms {251 users = append(users, m.Username)252 }253254 return nil255 }); err != nil {256 return nil, db.WrapError(err)257 }258259 return users, nil260}261262// AddPublicKey adds a public key to a user.263//264// It implements backend.Backend.265func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {266 username = strings.ToLower(username)267 if err := utils.ValidateUsername(username); err != nil {268 return err269 }270271 return db.WrapError(272 d.db.TransactionContext(ctx, func(tx *db.Tx) error {273 return d.store.AddPublicKeyByUsername(ctx, tx, username, pk)274 }),275 )276}277278// CreateUser creates a new user.279//280// It implements backend.Backend.281func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {282 username = strings.ToLower(username)283 if err := utils.ValidateUsername(username); err != nil {284 return nil, err285 }286287 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {288 return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)289 }); err != nil {290 return nil, db.WrapError(err)291 }292293 return d.User(ctx, username)294}295296// DeleteUser deletes a user.297//298// It implements backend.Backend.299func (d *Backend) DeleteUser(ctx context.Context, username string) error {300 username = strings.ToLower(username)301 if err := utils.ValidateUsername(username); err != nil {302 return err303 }304305 return d.db.TransactionContext(ctx, func(tx *db.Tx) error {306 if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {307 return db.WrapError(err)308 }309310 return d.DeleteUserRepositories(ctx, username)311 })312}313314// RemovePublicKey removes a public key from a user.315//316// It implements backend.Backend.317func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {318 return db.WrapError(319 d.db.TransactionContext(ctx, func(tx *db.Tx) error {320 return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)321 }),322 )323}324325// ListPublicKeys lists the public keys of a user.326func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {327 username = strings.ToLower(username)328 if err := utils.ValidateUsername(username); err != nil {329 return nil, err330 }331332 var keys []ssh.PublicKey333 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {334 var err error335 keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)336 return err337 }); err != nil {338 return nil, db.WrapError(err)339 }340341 return keys, nil342}343344// SetUsername sets the username of a user.345//346// It implements backend.Backend.347func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) error {348 username = strings.ToLower(username)349 if err := utils.ValidateUsername(username); err != nil {350 return err351 }352353 return db.WrapError(354 d.db.TransactionContext(ctx, func(tx *db.Tx) error {355 return d.store.SetUsernameByUsername(ctx, tx, username, newUsername)356 }),357 )358}359360// SetAdmin sets the admin flag of a user.361//362// It implements backend.Backend.363func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {364 username = strings.ToLower(username)365 if err := utils.ValidateUsername(username); err != nil {366 return err367 }368369 return db.WrapError(370 d.db.TransactionContext(ctx, func(tx *db.Tx) error {371 return d.store.SetAdminByUsername(ctx, tx, username, admin)372 }),373 )374}375376// SetPassword sets the password of a user.377func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {378 username = strings.ToLower(username)379 if err := utils.ValidateUsername(username); err != nil {380 return err381 }382383 password, err := HashPassword(rawPassword)384 if err != nil {385 return err386 }387388 return db.WrapError(389 d.db.TransactionContext(ctx, func(tx *db.Tx) error {390 return d.store.SetUserPasswordByUsername(ctx, tx, username, password)391 }),392 )393}394395type user struct {396 user models.User397 publicKeys []ssh.PublicKey398}399400var _ proto.User = (*user)(nil)401402// IsAdmin implements proto.User403func (u *user) IsAdmin() bool {404 return u.user.Admin405}406407// PublicKeys implements proto.User408func (u *user) PublicKeys() []ssh.PublicKey {409 return u.publicKeys410}411412// Username implements proto.User413func (u *user) Username() string {414 return u.user.Username415}416417// ID implements proto.User.418func (u *user) ID() int64 {419 return u.user.ID420}421422// Password implements proto.User.423func (u *user) Password() string {424 if u.user.Password.Valid {425 return u.user.Password.String426 }427428 return ""429}