diff --git a/server/apiLinstrom.go b/server/apiLinstrom.go index 80771ab..ea95d8b 100644 --- a/server/apiLinstrom.go +++ b/server/apiLinstrom.go @@ -42,6 +42,7 @@ func setupLinstromApiV1Router() http.Handler { router.HandleFunc("GET /accounts/{accountId}", linstromGetAccount) // Technically also requires authenticated account to also be owner or correct admin perms, // but that's annoying to handle in a general sense. So leaving that to the function + // though figuring out a nice generic-ish way to handle those checks would be nice too router.HandleFunc( "PATCH /accounts/{accountId}", requireValidSessionMiddleware(linstromUpdateAccount), diff --git a/server/apiLinstromAccounts.go b/server/apiLinstromAccounts.go index 9852b16..90f04ce 100644 --- a/server/apiLinstromAccounts.go +++ b/server/apiLinstromAccounts.go @@ -81,6 +81,89 @@ func linstromGetAccount(w http.ResponseWriter, r *http.Request) { func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) { store := StorageFromRequest(r) log := hlog.FromRequest(r) + // Assumption: There must be a valid session once this function is called due to middlewares + actorId, _ := ActorIdFromRequest(r) + apiTarget := linstromAccount{} + err := jsonapi.UnmarshalPayload(r.Body, &apiTarget) + if err != nil { + other.HttpErr(w, HttpErrIdBadRequest, "bad body", http.StatusBadRequest) + return + } + targetAccId := AccountIdFromRequest(r) + if apiTarget.Id != targetAccId { + other.HttpErr( + w, + HttpErrIdBadRequest, + "Provided entity's id doesn't match path id", + http.StatusConflict, + ) + return + } + if !(actorId == apiTarget.Id) { + other.HttpErr(w, HttpErrIdNotAuthenticated, "Invalid permissions", http.StatusForbidden) + return + } + dbTarget, err := store.FindAccountById(apiTarget.Id) + // Assumption: The only sort of errors that can be returned are db failures. + // The account not existing is not possible anymore since this is in a valid session + // and a session is only injected if the actor account can be found + if err != nil { + log.Error(). + Err(err). + Str("account-id", actorId). + Msg("Failed to get account from db despite valid session") + other.HttpErr( + w, + HttpErrIdDbFailure, + "Failed to get account despite valid session", + http.StatusInternalServerError, + ) + return + } + + // location, birthday, icon, banner, background, custom fields + // bluesky federation, uhhh + + dbTarget.DisplayName = apiTarget.DisplayName + dbTarget.Indexable = apiTarget.Indexable + dbTarget.Description = apiTarget.Description + // TODO: Figure out how to properly update custom fields + dbTarget.Gender = apiTarget.Pronouns + dbTarget.IdentifiesAs = sliceutils.Map( + sliceutils.Filter(apiTarget.IdentifiesAs, func(t string) bool { + return storage.IsValidBeing(t) + }), + func(t string) storage.Being { return storage.Being(t) }, + ) + dbTarget.Indexable = apiTarget.Indexable + dbTarget.RestrictedFollow = apiTarget.RestrictedFollow + err = store.UpdateAccount(dbTarget) + if err != nil { + log.Error().Err(err).Msg("Failed to update account in db") + other.HttpErr( + w, + HttpErrIdDbFailure, + "Failed to update db entries", + http.StatusInternalServerError, + ) + return + } + w.WriteHeader(http.StatusOK) + newAccData, err := convertAccountStorageToLinstrom(dbTarget, store) + if err != nil { + log.Error().Err(err).Msg("Failed to convert updated account back into api form") + other.HttpErr( + w, + HttpErrIdConverionFailure, + "Failed to convert updated account back into api form", + http.StatusInternalServerError, + ) + return + } + err = jsonapi.MarshalPayload(w, newAccData) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal and write updated account") + } } func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {} diff --git a/server/apiLinstromTypeHelpers.go b/server/apiLinstromTypeHelpers.go index b2cc272..a47ffeb 100644 --- a/server/apiLinstromTypeHelpers.go +++ b/server/apiLinstromTypeHelpers.go @@ -1,6 +1,8 @@ package server import ( + "fmt" + "gitlab.com/mstarongitlab/goutils/sliceutils" "gitlab.com/mstarongitlab/linstrom/storage" ) @@ -11,23 +13,23 @@ func convertAccountStorageToLinstrom( ) (*linstromAccount, error) { storageServer, err := store.FindRemoteServerById(acc.ServerId) if err != nil { - return nil, err + return nil, fmt.Errorf("remote server: %w", err) } apiServer, err := convertServerStorageToLinstrom(storageServer, store) if err != nil { - return nil, err + return nil, fmt.Errorf("remote server conversion: %w", err) } storageIcon, err := store.GetMediaMetadataById(acc.Icon) if err != nil { - return nil, err + return nil, fmt.Errorf("icon: %w", err) } storageBanner, err := store.GetMediaMetadataById(acc.Banner) if err != nil { - return nil, err + return nil, fmt.Errorf("banner: %w", err) } storageFields, err := store.FindMultipleUserFieldsById(acc.CustomFields) if err != nil { - return nil, err + return nil, fmt.Errorf("customFields: %w", err) } return &linstromAccount{ @@ -68,7 +70,7 @@ func convertServerStorageToLinstrom( ) (*linstromOriginServer, error) { storageMeta, err := store.GetMediaMetadataById(server.Icon) if err != nil { - return nil, err + return nil, fmt.Errorf("icon metadata: %w", err) } return &linstromOriginServer{ Id: server.ID, diff --git a/server/apiRouter.go b/server/apiRouter.go index f15538a..75591f0 100644 --- a/server/apiRouter.go +++ b/server/apiRouter.go @@ -5,7 +5,7 @@ import "net/http" // Mounted at /api func setupApiRouter() http.Handler { router := http.NewServeMux() - router.Handle("/linstrom/", setupLinstromApiRouter()) + router.Handle("/linstrom/", http.StripPrefix("/linstrom", setupLinstromApiRouter())) // Section MastoApi // First segment are endpoints that will need to be moved to primary router since at top route diff --git a/server/middlewareFixPasskeyPerms.go b/server/middlewareFixPasskeyPerms.go index b163905..a09ddb6 100644 --- a/server/middlewareFixPasskeyPerms.go +++ b/server/middlewareFixPasskeyPerms.go @@ -103,6 +103,7 @@ func fuckWithRegisterRequest( } else { // Not authenticated, ensure that no existing name is registered with _, err = store.FindLocalAccountByUsername(username.Username) + log.Debug().Bool("err-equals-not_found", err == storage.ErrEntryNotFound).Send() switch err { case nil: // No error while getting account means account exists, refuse access diff --git a/server/utils.go b/server/utils.go index a63bb5a..cae8e6d 100644 --- a/server/utils.go +++ b/server/utils.go @@ -27,6 +27,11 @@ func StorageFromRequest(r *http.Request) *storage.Storage { return store } +func ActorIdFromRequest(r *http.Request) (string, bool) { + id, ok := r.Context().Value(ContextKeyActorId).(string) + return id, ok +} + func NoteIdFromRequest(r *http.Request) string { return r.PathValue("noteId") } @@ -34,3 +39,16 @@ func NoteIdFromRequest(r *http.Request) string { func AccountIdFromRequest(r *http.Request) string { return r.PathValue("accountId") } + +func CheckIfAccountIdHasPermissions(accId string, perms storage.Role, store *storage.Storage) bool { + acc, err := store.FindAccountById(accId) + if err != nil { + return false + } + roles, err := store.FindRolesByNames(acc.Roles) + if err != nil { + return false + } + collapsed := storage.CollapseRolesIntoOne(roles...) + return storage.CompareRoles(&collapsed, &perms) +} diff --git a/storage/roles.go b/storage/roles.go index 172c45d..f0d52a6 100644 --- a/storage/roles.go +++ b/storage/roles.go @@ -22,14 +22,14 @@ type Role struct { gorm.Model // Name of the role - Name string `gorm:"primaryKey"` + Name string `gorm:"primaryKey;unique"` // Priority of the role // Lower priority gets applied first and thus overwritten by higher priority ones // If two roles have the same priority, the order is undetermined and may be random // Default priority for new roles is 1 to always overwrite default user // And full admin has max priority possible - Priority uint + Priority uint32 // Whether this role is for a for a single user only (like custom, per user permissions in Discord) // If yes, Name will be the id of the user in question IsUserRole bool diff --git a/storage/rolesDefaults.go b/storage/rolesDefaults.go index 1989587..a40b2ac 100644 --- a/storage/rolesDefaults.go +++ b/storage/rolesDefaults.go @@ -69,7 +69,7 @@ var DefaultUserRole = Role{ // Role providing maximum permissions var FullAdminRole = Role{ Name: "fullAdmin", - Priority: math.MaxUint, + Priority: math.MaxUint32, IsUserRole: false, IsBuiltIn: true, @@ -125,7 +125,7 @@ var FullAdminRole = Role{ // Role for totally freezing an account, blocking all activity from it var AccountFreezeRole = Role{ Name: "accountFreeze", - Priority: math.MaxUint - 1, + Priority: math.MaxUint32 - 1, IsUserRole: false, IsBuiltIn: true, @@ -181,8 +181,64 @@ var AccountFreezeRole = Role{ CanSendAnnouncements: other.IntoPointer(false), } +var ServerActorRole = Role{ + Name: "ServerActor", + Priority: math.MaxUint32, + IsUserRole: true, + IsBuiltIn: true, + + CanSendMedia: other.IntoPointer(true), + CanSendCustomEmotes: other.IntoPointer(true), + CanSendCustomReactions: other.IntoPointer(true), + CanSendPublicNotes: other.IntoPointer(true), + CanSendLocalNotes: other.IntoPointer(true), + CanSendFollowerOnlyNotes: other.IntoPointer(true), + CanSendPrivateNotes: other.IntoPointer(true), + CanQuote: other.IntoPointer(true), + CanBoost: other.IntoPointer(true), + CanIncludeLinks: other.IntoPointer(true), + CanIncludeSurvey: other.IntoPointer(true), + + CanChangeDisplayName: other.IntoPointer(true), + + BlockedUsers: []string{}, + CanSubmitReports: other.IntoPointer(true), + CanLogin: other.IntoPointer(true), + + CanMentionOthers: other.IntoPointer(true), + HasMentionCountLimit: other.IntoPointer(false), + MentionLimit: other.IntoPointer( + uint32(math.MaxUint32), + ), // Set this to max, even if not used due to *HasMentionCountLimit == false + + AutoNsfwMedia: other.IntoPointer(false), + AutoCwPosts: other.IntoPointer(false), + AutoCwPostsText: nil, + WithholdNotesForManualApproval: other.IntoPointer(false), + ScanCreatedPublicNotes: other.IntoPointer(false), + ScanCreatedLocalNotes: other.IntoPointer(false), + ScanCreatedFollowerOnlyNotes: other.IntoPointer(false), + ScanCreatedPrivateNotes: other.IntoPointer(false), + DisallowInteractionsWith: []string{}, + + FullAdmin: other.IntoPointer(true), + CanAffectOtherAdmins: other.IntoPointer(true), + CanDeleteNotes: other.IntoPointer(true), + CanConfirmWithheldNotes: other.IntoPointer(true), + CanAssignRoles: other.IntoPointer(true), + CanSupressInteractionsBetweenUsers: other.IntoPointer(true), + CanOverwriteDisplayNames: other.IntoPointer(true), + CanManageCustomEmotes: other.IntoPointer(true), + CanViewDeletedNotes: other.IntoPointer(true), + CanRecoverDeletedNotes: other.IntoPointer(true), + CanManageAvatarDecorations: other.IntoPointer(true), + CanManageAds: other.IntoPointer(true), + CanSendAnnouncements: other.IntoPointer(true), +} + var allDefaultRoles = []*Role{ &DefaultUserRole, &FullAdminRole, &AccountFreezeRole, + &ServerActorRole, } diff --git a/storage/storage.go b/storage/storage.go index 4eb8ea5..06c98c0 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -53,8 +53,11 @@ func NewStorage(dbUrl string, cache *cache.Cache) (*Storage, error) { s := &Storage{db, cache} + if err = s.insertDefaultRoles(); err != nil { + return nil, fmt.Errorf("default roles insertion failed: %w", err) + } if err = s.insertSelfFromConfig(); err != nil { - return nil, err + return nil, fmt.Errorf("self insertion failed: %w", err) } return s, nil @@ -68,12 +71,15 @@ func (s *Storage) insertSelfFromConfig() error { // Insert server info serverData := RemoteServer{} err = s.db.Where("id = 1"). + // Set once on creation Attrs(RemoteServer{ Domain: config.GlobalConfig.General.GetFullDomain(), }). + // Set every time Assign(RemoteServer{ - IsSelf: true, - Name: config.GlobalConfig.Self.ServerDisplayName, + IsSelf: true, + Name: config.GlobalConfig.Self.ServerDisplayName, + ServerType: REMOTE_SERVER_LINSTROM, // Icon: "", // TODO: Set to server icon media }).FirstOrCreate(&serverData).Error if err != nil { @@ -91,6 +97,7 @@ func (s *Storage) insertSelfFromConfig() error { err = s.db.Where(Account{ID: ServerActorId}). // Values to always (re)set after launch Assign(Account{ + Username: "self", DisplayName: config.GlobalConfig.Self.ServerActorDisplayName, // Server: serverData, ServerId: serverData.ID, @@ -107,7 +114,7 @@ func (s *Storage) insertSelfFromConfig() error { RestrictedFollow: false, IdentifiesAs: []Being{}, Gender: []string{}, - Roles: []string{}, // TODO: Add server actor role once created + Roles: []string{"ServerActor"}, // TODO: Add server actor role once created }). // Values that'll only be set on first creation Attrs(Account{ @@ -120,3 +127,13 @@ func (s *Storage) insertSelfFromConfig() error { } return nil } + +func (s *Storage) insertDefaultRoles() error { + for _, role := range allDefaultRoles { + log.Debug().Str("role-name", role.Name).Msg("Inserting default role") + if err := s.db.FirstOrCreate(role).Error; err != nil { + return err + } + } + return nil +} diff --git a/storage/user.go b/storage/user.go index 509987b..bd444fb 100644 --- a/storage/user.go +++ b/storage/user.go @@ -13,7 +13,6 @@ import ( "github.com/mstarongithub/passkey" "github.com/rs/zerolog/log" "gitlab.com/mstarongitlab/linstrom/ap" - "gitlab.com/mstarongitlab/linstrom/config" "gorm.io/gorm" ) @@ -61,6 +60,9 @@ type Account struct { // The roles assocciated with an account. Values are the names of the roles Roles []string `gorm:"serializer:json"` + Location *string + Birthday *time.Time + // --- And internal account stuff --- // Still public fields since they wouldn't be able to be stored in the db otherwise PrivateKey []byte // The private key of the account. Nil if remote user @@ -166,7 +168,8 @@ func (s *Storage) FindAccountById(id string) (*Account, error) { } log.Debug().Str("account-id", id).Msg("Didn't hit account in cache, checking db") - res := s.db.First(acc, id) + acc = &Account{ID: id} + res := s.db.First(acc) if res.Error != nil { if errors.Is(res.Error, gorm.ErrRecordNotFound) { log.Warn().Str("account-id", id).Msg("Account not found") @@ -194,7 +197,7 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) // Then always load via id since unique key access should be faster than string matching return s.FindAccountById(*cacheAccId) } else { - if !errors.Is(err, errCacheNotFound) { + if err != errCacheNotFound { log.Error().Err(err).Str("account-username", username).Msg("Problem while checking cache for account") return nil, err } @@ -202,17 +205,10 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) // Failed to find in cache, go the slow route of hitting the db log.Debug().Str("account-username", username).Msg("Didn't hit account in cache, going to db") - if err != nil { - log.Warn(). - Err(err). - Str("account-username", username). - Msg("Failed to split up account username") - return nil, err - } acc := Account{} res := s.db.Where("username = ?", username). - Where("server = ?", config.GlobalConfig.General.GetFullDomain()). + Where("server_id = ?", serverSelf.ID). First(&acc) if res.Error != nil { if errors.Is(res.Error, gorm.ErrRecordNotFound) { @@ -222,7 +218,7 @@ func (s *Storage) FindLocalAccountByUsername(username string) (*Account, error) } else { log.Error().Err(err).Str("account-username", username).Msg("Failed to get local account with username") } - return nil, res.Error + return nil, ErrEntryNotFound } log.Info().Str("account-username", username).Msg("Found account, also inserting into cache") if err = s.cache.Set(cacheUserIdToAccPrefix+acc.ID, &acc); err != nil { diff --git a/storage/userIdentType.go b/storage/userIdentType.go index 3693853..59b67ad 100644 --- a/storage/userIdentType.go +++ b/storage/userIdentType.go @@ -1,5 +1,7 @@ package storage +import "gitlab.com/mstarongitlab/goutils/sliceutils" + // What kind of being a user identifies as type Being string @@ -11,3 +13,9 @@ const ( BEING_ROBOT = Being("robot") BEING_DOLL = Being("doll") ) + +var allBeings = []Being{BEING_HUMAN, BEING_CAT, BEING_FOX, BEING_DOG, BEING_ROBOT, BEING_DOLL} + +func IsValidBeing(toCheck string) bool { + return sliceutils.Contains(allBeings, Being(toCheck)) +}