More API stuff. Lots of bleh. Really boring

Also need to figure out a somewhat generic way for "requires ownership"
permission and then a combinator for permissions
This commit is contained in:
Melody Becker 2024-11-04 16:25:39 +01:00
parent ffe3cf32ae
commit 1bb6cd8a70
8 changed files with 438 additions and 300 deletions

View file

@ -40,8 +40,18 @@ func setupLinstromApiV1Router() http.Handler {
// Accounts // Accounts
// Creating a new account happens either during fetch of a remote one or during registration with a passkey // Creating a new account happens either during fetch of a remote one or during registration with a passkey
router.HandleFunc("GET /accounts/{accountId}", linstromGetAccount) router.HandleFunc("GET /accounts/{accountId}", linstromGetAccount)
router.HandleFunc("PATCH /accounts/{accountId}", linstromUpdateAccount) // Technically also requires authenticated account to also be owner or correct admin perms,
router.HandleFunc("DELETE /accounts/{accountId}", linstromDeleteAccount) // but that's annoying to handle in a general sense. So leaving that to the function
router.HandleFunc(
"PATCH /accounts/{accountId}",
requireValidSessionMiddleware(linstromUpdateAccount),
)
// 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
router.HandleFunc(
"DELETE /accounts/{accountId}",
requireValidSessionMiddleware(linstromDeleteAccount),
)
// Follow // Follow
router.HandleFunc("GET /accounts/{accountId}/follow", linstromIsFollowingAccount) router.HandleFunc("GET /accounts/{accountId}/follow", linstromIsFollowingAccount)
router.HandleFunc("POST /accounts/{accountId}/follow", linstromFollowAccount) router.HandleFunc("POST /accounts/{accountId}/follow", linstromFollowAccount)

View file

@ -6,6 +6,7 @@ import (
"github.com/google/jsonapi" "github.com/google/jsonapi"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"gitlab.com/mstarongitlab/goutils/other" "gitlab.com/mstarongitlab/goutils/other"
"gitlab.com/mstarongitlab/goutils/sliceutils"
"gitlab.com/mstarongitlab/linstrom/storage" "gitlab.com/mstarongitlab/linstrom/storage"
) )
@ -14,6 +15,7 @@ import (
func linstromGetAccount(w http.ResponseWriter, r *http.Request) { func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
store := StorageFromRequest(r) store := StorageFromRequest(r)
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
accId := AccountIdFromRequest(r) accId := AccountIdFromRequest(r)
acc, err := store.FindAccountById(accId) acc, err := store.FindAccountById(accId)
switch err { switch err {
@ -32,7 +34,30 @@ func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
) )
return return
} }
// TODO: Check if caller is actually allowed to view the account requested. actorId, ok := r.Context().Value(ContextKeyActorId).(string)
if ok {
// Logged in user is accessing account, check if target account has them blocked
roles, err := store.FindRolesByNames(acc.Roles)
if err != nil {
log.Error().
Err(err).
Strs("role-names", acc.Roles).
Msg("Failed to get roles from storage")
other.HttpErr(
w,
HttpErrIdDbFailure,
"Failed to get roles of target account",
http.StatusInternalServerError,
)
return
}
collapsedRole := storage.CollapseRolesIntoOne(roles...)
if sliceutils.Contains(collapsedRole.BlockedUsers, actorId) {
// Actor account is in list of blocked accounts, deny access
other.HttpErr(w, HttpErrIdNotAuthenticated, "Access forbidden", http.StatusForbidden)
return
}
}
outAccount, err := convertAccountStorageToLinstrom(acc, store) outAccount, err := convertAccountStorageToLinstrom(acc, store)
if err != nil { if err != nil {
@ -53,7 +78,10 @@ func linstromGetAccount(w http.ResponseWriter, r *http.Request) {
} }
} }
func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {} func linstromUpdateAccount(w http.ResponseWriter, r *http.Request) {
store := StorageFromRequest(r)
log := hlog.FromRequest(r)
}
func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {} func linstromDeleteAccount(w http.ResponseWriter, r *http.Request) {}
func linstromIsFollowingAccount(w http.ResponseWriter, r *http.Request) {} func linstromIsFollowingAccount(w http.ResponseWriter, r *http.Request) {}

View file

@ -11,6 +11,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gitlab.com/mstarongitlab/goutils/other" "gitlab.com/mstarongitlab/goutils/other"
"gitlab.com/mstarongitlab/linstrom/config" "gitlab.com/mstarongitlab/linstrom/config"
"gitlab.com/mstarongitlab/linstrom/storage"
) )
type HandlerBuilder func(http.Handler) http.Handler type HandlerBuilder func(http.Handler) http.Handler
@ -152,3 +153,78 @@ func checkSessionMiddleware(handler http.Handler) http.Handler {
) )
}) })
} }
func requireValidSessionMiddleware(
h func(http.ResponseWriter, *http.Request),
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
_, ok := r.Context().Value(ContextKeyActorId).(string)
if !ok {
other.HttpErr(
w,
HttpErrIdNotAuthenticated,
"Not authenticated",
http.StatusUnauthorized,
)
return
}
h(w, r)
}
}
func buildRequirePermissionsMiddleware(permissionRole *storage.Role) HandlerBuilder {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accId, ok := r.Context().Value(ContextKeyActorId).(string)
if !ok {
other.HttpErr(
w,
HttpErrIdNotAuthenticated,
"Not authenticated",
http.StatusUnauthorized,
)
return
}
store := StorageFromRequest(r)
log := hlog.FromRequest(r)
acc, err := store.FindAccountById(accId)
// Assumption: If this handler is hit, the middleware for checking if a session exists at all has already passed
// and thus a valid account id must exist in the context
if err != nil {
log.Error().
Err(err).
Str("account-id", accId).
Msg("Error while getting account from session")
other.HttpErr(
w,
HttpErrIdDbFailure,
"Error while getting account from session",
http.StatusInternalServerError,
)
return
}
roles, err := store.FindRolesByNames(acc.Roles)
// Assumption: There will always be at least two roles per user, default user and user-specific one
if err != nil {
other.HttpErr(
w,
HttpErrIdDbFailure,
"Failed to get roles for account",
http.StatusInternalServerError,
)
return
}
collapsedRole := storage.CollapseRolesIntoOne(roles...)
if !storage.CompareRoles(&collapsedRole, permissionRole) {
other.HttpErr(
w,
HttpErrIdNotAuthenticated,
"Insufficient permisions",
http.StatusForbidden,
)
return
}
h.ServeHTTP(w, r)
})
}
}

View file

@ -38,7 +38,7 @@ func buildRootHandler(pkey *passkey.Passkey, reactiveFS, staticFS fs.FS) http.Ha
mux.Handle("/", setupFrontendRouter(reactiveFS, staticFS)) mux.Handle("/", setupFrontendRouter(reactiveFS, staticFS))
mux.Handle("/pk/", http.StripPrefix("/pk", http.FileServer(http.Dir("pk-auth")))) mux.Handle("/pk/", http.StripPrefix("/pk", http.FileServer(http.Dir("pk-auth"))))
mux.HandleFunc("/alive", isAliveHandler) mux.HandleFunc("/alive", isAliveHandler)
mux.Handle("/api/", http.StripPrefix("/api", setupApiRouter())) mux.Handle("/api/", http.StripPrefix("/api", checkSessionMiddleware(setupApiRouter())))
mux.Handle( mux.Handle(
"/profiling/", "/profiling/",

View file

@ -87,7 +87,7 @@ func (s *Storage) UpdateRemoteServer(url string, displayName, icon *string) (*Re
if displayName == nil && icon == nil { if displayName == nil && icon == nil {
return nil, ErrNothingToChange return nil, ErrNothingToChange
} }
server, err := s.FindRemoteServer(url) server, err := s.FindRemoteServerByDomain(url)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -39,30 +39,31 @@ type Role struct {
IsBuiltIn bool IsBuiltIn bool
// --- User permissions --- // --- User permissions ---
CanSendMedia *bool CanSendMedia *bool // Local & remote
CanSendCustomEmotes *bool CanSendCustomEmotes *bool // Local & remote
CanSendCustomReactions *bool CanSendCustomReactions *bool // Local & remote
CanSendPublicNotes *bool CanSendPublicNotes *bool // Local & remote
CanSendLocalNotes *bool CanSendLocalNotes *bool // Local & remote
CanSendFollowerOnlyNotes *bool CanSendFollowerOnlyNotes *bool // Local & remote
CanSendPrivateNotes *bool CanSendPrivateNotes *bool // Local & remote
CanSendReplies *bool CanSendReplies *bool // Local & remote
CanQuote *bool CanQuote *bool // Local only
CanBoost *bool CanBoost *bool // Local only
CanIncludeLinks *bool CanIncludeLinks *bool // Local & remote
CanIncludeSurvey *bool CanIncludeSurvey *bool // Local
CanFederateFedi *bool CanFederateFedi *bool // Local & remote
CanFederateBsky *bool CanFederateBsky *bool // Local
CanChangeDisplayName *bool CanChangeDisplayName *bool // Local
BlockedUsers []string `gorm:"type:bytes;serializer:gob"` // Internal ids of accounts blocked by this role
CanSubmitReports *bool BlockedUsers []string `gorm:"type:bytes;serializer:gob"` // Local
CanLogin *bool CanSubmitReports *bool // Local & remote
CanLogin *bool // Local
CanMentionOthers *bool CanMentionOthers *bool // Local & remote
HasMentionCountLimit *bool HasMentionCountLimit *bool // Local & remote
MentionLimit *uint32 MentionLimit *uint32 // Local & remote
// CanViewBoosts *bool // CanViewBoosts *bool
// CanViewQuotes *bool // CanViewQuotes *bool
@ -70,39 +71,39 @@ type Role struct {
// CanViewCustomEmotes *bool // CanViewCustomEmotes *bool
// --- Automod --- // --- Automod ---
AutoNsfwMedia *bool AutoNsfwMedia *bool // Local & remote
AutoCwPosts *bool AutoCwPosts *bool // Local & remote
AutoCwPostsText *string AutoCwPostsText *string // Local & remote
ScanCreatedPublicNotes *bool ScanCreatedPublicNotes *bool // Local & remote
ScanCreatedLocalNotes *bool ScanCreatedLocalNotes *bool // Local & remote
ScanCreatedFollowerOnlyNotes *bool ScanCreatedFollowerOnlyNotes *bool // Local & remote
ScanCreatedPrivateNotes *bool ScanCreatedPrivateNotes *bool // Local & remote
// Blocks all interactions and federation between users with the role and all included ids/handles // Blocks all interactions and federation between users with the role and all included ids/handles
// TODO: Decide whether this is a list of handles or of account ids // TODO: Decide whether this is a list of handles or of account ids
// Handles would increase the load due to having to search for them first // Handles would increase the load due to having to search for them first
// while ids would require to store every single account mentioned // while ids would require to store every single account mentioned
// which could cause escalating storage costs // which could cause escalating storage costs
DisallowInteractionsWith []string `gorm:"type:bytes;serializer:gob"` DisallowInteractionsWith []string `gorm:"type:bytes;serializer:gob"` // Local & remote
WithholdNotesForManualApproval *bool WithholdNotesForManualApproval *bool // Local & remote
WithholdNotesBasedOnRegex *bool WithholdNotesBasedOnRegex *bool // Local & remote
WithholdNotesRegexes []string `gorm:"type:bytes;serializer:gob"` WithholdNotesRegexes []string `gorm:"type:bytes;serializer:gob"` // Local & remote
// --- Admin perms --- // --- Admin perms ---
// If set, counts as all permissions being set as given and all restrictions being disabled // If set, counts as all permissions being set as given and all restrictions being disabled
FullAdmin *bool FullAdmin *bool // Local
CanAffectOtherAdmins *bool CanAffectOtherAdmins *bool // Local
CanDeleteNotes *bool CanDeleteNotes *bool // Local
CanConfirmWithheldNotes *bool CanConfirmWithheldNotes *bool // Local
CanAssignRoles *bool CanAssignRoles *bool // Local
CanSupressInteractionsBetweenUsers *bool CanSupressInteractionsBetweenUsers *bool // Local
CanOverwriteDisplayNames *bool CanOverwriteDisplayNames *bool // Local
CanManageCustomEmotes *bool CanManageCustomEmotes *bool // Local
CanViewDeletedNotes *bool CanViewDeletedNotes *bool // Local
CanRecoverDeletedNotes *bool CanRecoverDeletedNotes *bool // Local
CanManageAvatarDecorations *bool CanManageAvatarDecorations *bool // Local
CanManageAds *bool CanManageAds *bool // Local
CanSendAnnouncements *bool CanSendAnnouncements *bool // Local
} }
/* /*
@ -169,7 +170,8 @@ func (s *Storage) NewEmptyRole(name string) (*Role, error) {
return nil, err return nil, err
} }
newRole := Role{Name: name} // New roles have a priority of 1 by default
newRole := Role{Name: name, Priority: 1}
err = s.db.Create(&newRole).Error err = s.db.Create(&newRole).Error
if err != nil { if err != nil {
return nil, err return nil, err
@ -189,3 +191,16 @@ func (s *Storage) FindRoleByName(name string) (*Role, error) {
return nil, err return nil, err
} }
} }
func (s *Storage) FindRolesByNames(names []string) ([]Role, error) {
roles := []Role{}
err := s.db.Where("name IN ?", names).Find(&roles).Error
switch err {
case nil:
return roles, nil
case gorm.ErrRecordNotFound:
return nil, ErrEntryNotFound
default:
return nil, err
}
}

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,7 @@ import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
@ -57,7 +58,7 @@ type Account struct {
// An unordered list since the owner can freely set it // An unordered list since the owner can freely set it
// Examples: [she her], [it they its them] or, if you want to go fancy, [this is super serious] // Examples: [she her], [it they its them] or, if you want to go fancy, [this is super serious]
Gender []string `gorm:"serializer:json"` Gender []string `gorm:"serializer:json"`
// The roles assocciated with an account // The roles assocciated with an account. Values are the names of the roles
Roles []string `gorm:"serializer:json"` Roles []string `gorm:"serializer:json"`
// --- And internal account stuff --- // --- And internal account stuff ---
@ -320,6 +321,12 @@ func (s *Storage) NewEmptyAccount() (*Account, error) {
} }
log.Debug().Msg("Random webauthn id for new account created") log.Debug().Msg("Random webauthn id for new account created")
acc.ID = uuid.NewString() acc.ID = uuid.NewString()
accountRole, err := s.NewEmptyRole(acc.ID)
if err != nil {
return nil, fmt.Errorf("failed to generate account role for new account: %w", err)
}
acc.WebAuthnId = data acc.WebAuthnId = data
acc.Followers = []string{} acc.Followers = []string{}
acc.Tags = []string{} acc.Tags = []string{}
@ -328,6 +335,7 @@ func (s *Storage) NewEmptyAccount() (*Account, error) {
acc.CustomFields = []uint{} acc.CustomFields = []uint{}
acc.IdentifiesAs = []Being{} acc.IdentifiesAs = []Being{}
acc.PasskeyCredentials = []webauthn.Credential{} acc.PasskeyCredentials = []webauthn.Credential{}
acc.Roles = []string{DefaultUserRole.Name, accountRole.Name}
log.Debug().Any("account", &acc).Msg("Saving new account in db") log.Debug().Any("account", &acc).Msg("Saving new account in db")
res := s.db.Save(&acc) res := s.db.Save(&acc)
if res.Error != nil { if res.Error != nil {