memos/server/router/api/v1/memo_access.go

183 lines
6.7 KiB
Go

package v1
import (
"context"
stderrors "errors"
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/usememos/memos/server/access"
"github.com/usememos/memos/store"
)
// buildMemoReadContext resolves authorization inputs for exactly one memo.
// Relations never contribute access to either endpoint.
func (s *APIV1Service) buildMemoReadContext(ctx context.Context, memo *store.Memo, sharedMemoID *int32) (access.MemoReadContext, error) {
viewer, err := s.fetchCurrentUser(ctx)
if err != nil {
return access.MemoReadContext{}, status.Errorf(codes.Internal, "failed to get user")
}
allowAnonymous := false
if viewer == nil {
allowAnonymous, err = s.Store.AllowsAnonymousAccess(ctx)
if err != nil {
return access.MemoReadContext{}, status.Errorf(codes.Internal, "failed to resolve instance access policy")
}
}
return s.buildMemoReadContextForViewer(ctx, memo, viewer, allowAnonymous, sharedMemoID)
}
func (s *APIV1Service) buildMemoReadContextForViewer(ctx context.Context, memo *store.Memo, viewer *store.User, allowAnonymous bool, sharedMemoID *int32) (access.MemoReadContext, error) {
if memo == nil {
return access.MemoReadContext{}, status.Error(codes.NotFound, "memo not found")
}
readContext, err := access.ResolveMemoReadContext(ctx, s.Store, memo, viewer, allowAnonymous, sharedMemoID)
if err != nil {
return access.MemoReadContext{}, status.Errorf(codes.Internal, "failed to resolve memo access")
}
return readContext, nil
}
func (s *APIV1Service) checkMemoReadAccess(ctx context.Context, memo *store.Memo) error {
readContext, err := s.buildMemoReadContext(ctx, memo, nil)
if err != nil {
return err
}
return memoAccessDecisionError(access.CheckMemoReadContext(readContext))
}
func memoAccessDecisionError(decision access.MemoReadDecision) error {
switch decision.Denial {
case access.MemoReadDenialNone:
return nil
case access.MemoReadDenialNotFound:
return status.Error(codes.NotFound, "memo not found")
case access.MemoReadDenialUnauthenticated:
return status.Error(codes.Unauthenticated, "user not authenticated")
default:
return status.Error(codes.PermissionDenied, "permission denied")
}
}
// newMemoAccessScope returns the memo-local authorization predicate for a
// caller. Drivers apply it as a database predicate before LIMIT/OFFSET so
// inaccessible rows can neither leak nor skew counts and pagination.
func newMemoAccessScope(currentUser *store.User, allowPublic bool) *store.MemoAccessScope {
accessScope := &store.MemoAccessScope{AllowPublic: allowPublic, AllowProtected: currentUser != nil}
if currentUser != nil {
accessScope.UserID = &currentUser.ID
}
return accessScope
}
// resolveMemoAccessScope resolves the caller and builds their memo access
// scope. For an anonymous caller the instance access policy decides whether
// PUBLIC memos are readable at all. Callers map the returned error to their own
// transport representation.
func (s *APIV1Service) resolveMemoAccessScope(ctx context.Context) (*store.MemoAccessScope, *store.User, error) {
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get current user")
}
allowPublic := currentUser != nil
if currentUser == nil {
allowPublic, err = s.Store.AllowsAnonymousAccess(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to resolve instance access policy")
}
}
return newMemoAccessScope(currentUser, allowPublic), currentUser, nil
}
// resolveWritableSpaceByName resolves a space resource name and requires the
// caller to be an active member of it. A non-member receives NotFound so that
// an existing collaboration boundary stays indistinguishable from a missing
// resource.
func (s *APIV1Service) resolveWritableSpaceByName(ctx context.Context, name string, userID int32) (*store.Space, error) {
spaceUID, err := ExtractSpaceUIDFromName(name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid space name: %v", err)
}
space, err := s.Store.GetSpace(ctx, &store.FindSpace{UID: &spaceUID})
if err != nil {
return nil, status.Error(codes.Internal, "failed to get space")
}
if space == nil {
return nil, status.Error(codes.NotFound, "space not found")
}
active, err := s.isActiveSpaceMember(ctx, space.ID, userID)
if err != nil {
return nil, status.Error(codes.Internal, "failed to resolve space membership")
}
if !active {
return nil, status.Error(codes.NotFound, "space not found")
}
return space, nil
}
func (s *APIV1Service) isActiveSpaceMember(ctx context.Context, spaceID, userID int32) (bool, error) {
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
if err != nil {
return false, err
}
if user == nil || user.RowStatus != store.Normal {
return false, nil
}
membership, err := s.Store.GetSpaceMember(ctx, &store.FindSpaceMember{SpaceID: &spaceID, UserID: &userID})
if err != nil {
return false, err
}
return membership != nil && membership.Role.IsActiveMember(), nil
}
func sameOptionalInt32(left, right *int32) bool {
if left == nil || right == nil {
return left == nil && right == nil
}
return *left == *right
}
func memoWritePolicy(actorUserID int32, lifecycleOnly bool) *store.MemoWritePolicy {
return &store.MemoWritePolicy{
ActorUserID: actorUserID,
LifecycleOnly: lifecycleOnly,
}
}
func mapMemoWriteError(err error, operation string) error {
switch {
case stderrors.Is(err, store.ErrMemoMutationConflict):
return status.Errorf(codes.FailedPrecondition, "memo state changed: %v", err)
case stderrors.Is(err, store.ErrMemoSpaceNotWritable):
return status.Error(codes.FailedPrecondition, "memo space is no longer writable")
case stderrors.Is(err, store.ErrMemoSpaceMembershipRequired), stderrors.Is(err, store.ErrMemoPermissionDenied):
return status.Error(codes.PermissionDenied, "permission denied")
case stderrors.Is(err, store.ErrMemoShareConflict):
return status.Error(codes.FailedPrecondition, "revoke active shares before using the SPACE audience")
default:
return status.Errorf(codes.Internal, "%s: %v", operation, err)
}
}
func (s *APIV1Service) requireAssignedMemoWritable(ctx context.Context, memo *store.Memo, userID int32) error {
if memo.SpaceID == nil {
return nil
}
space, err := s.Store.GetSpace(ctx, &store.FindSpace{ID: memo.SpaceID})
if err != nil {
return status.Errorf(codes.Internal, "failed to get memo space")
}
if space == nil {
return status.Errorf(codes.FailedPrecondition, "memo has invalid space placement")
}
active, err := s.isActiveSpaceMember(ctx, space.ID, userID)
if err != nil {
return status.Errorf(codes.Internal, "failed to resolve space membership")
}
if !active {
return status.Errorf(codes.PermissionDenied, "active space membership is required")
}
return nil
}