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 = utils.Sanitize(username)283 username = strings.ToLower(username)284 if err := utils.ValidateUsername(username); err != nil {285 return nil, err286 }287288 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {289 return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)290 }); err != nil {291 return nil, db.WrapError(err)292 }293294 return d.User(ctx, username)295}296297// DeleteUser deletes a user.298//299// It implements backend.Backend.300func (d *Backend) DeleteUser(ctx context.Context, username string) error {301 username = strings.ToLower(username)302 if err := utils.ValidateUsername(username); err != nil {303 return err304 }305306 return d.db.TransactionContext(ctx, func(tx *db.Tx) error {307 if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {308 return db.WrapError(err)309 }310311 return d.DeleteUserRepositories(ctx, username)312 })313}314315// RemovePublicKey removes a public key from a user.316//317// It implements backend.Backend.318func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {319 return db.WrapError(320 d.db.TransactionContext(ctx, func(tx *db.Tx) error {321 return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)322 }),323 )324}325326// ListPublicKeys lists the public keys of a user.327func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {328 username = strings.ToLower(username)329 if err := utils.ValidateUsername(username); err != nil {330 return nil, err331 }332333 var keys []ssh.PublicKey334 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {335 var err error336 keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)337 return err338 }); err != nil {339 return nil, db.WrapError(err)340 }341342 return keys, nil343}344345// SetUsername sets the username of a user.346//347// It implements backend.Backend.348func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) error {349 username = strings.ToLower(username)350 if err := utils.ValidateUsername(username); err != nil {351 return err352 }353354 return db.WrapError(355 d.db.TransactionContext(ctx, func(tx *db.Tx) error {356 return d.store.SetUsernameByUsername(ctx, tx, username, newUsername)357 }),358 )359}360361// SetAdmin sets the admin flag of a user.362//363// It implements backend.Backend.364func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {365 username = strings.ToLower(username)366 if err := utils.ValidateUsername(username); err != nil {367 return err368 }369370 return db.WrapError(371 d.db.TransactionContext(ctx, func(tx *db.Tx) error {372 return d.store.SetAdminByUsername(ctx, tx, username, admin)373 }),374 )375}376377// SetPassword sets the password of a user.378func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {379 username = strings.ToLower(username)380 if err := utils.ValidateUsername(username); err != nil {381 return err382 }383384 password, err := HashPassword(rawPassword)385 if err != nil {386 return err387 }388389 return db.WrapError(390 d.db.TransactionContext(ctx, func(tx *db.Tx) error {391 return d.store.SetUserPasswordByUsername(ctx, tx, username, password)392 }),393 )394}395396type user struct {397 user models.User398 publicKeys []ssh.PublicKey399}400401var _ proto.User = (*user)(nil)402403// IsAdmin implements proto.User404func (u *user) IsAdmin() bool {405 return u.user.Admin406}407408// PublicKeys implements proto.User409func (u *user) PublicKeys() []ssh.PublicKey {410 return u.publicKeys411}412413// Username implements proto.User414func (u *user) Username() string {415 return u.user.Username416}417418// ID implements proto.User.419func (u *user) ID() int64 {420 return u.user.ID421}422423// Password implements proto.User.424func (u *user) Password() string {425 if u.user.Password.Valid {426 return u.user.Password.String427 }428429 return ""430}