bleh
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:
parent
ffe3cf32ae
commit
1bb6cd8a70
8 changed files with 438 additions and 300 deletions
|
@ -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)
|
||||||
|
|
|
@ -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) {}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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/",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
107
storage/roles.go
107
storage/roles.go
|
@ -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
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue