Files
notely/backend/internal/application/services/permission_service.go
2026-03-26 16:27:14 +00:00

175 lines
4.9 KiB
Go

package services
import (
"context"
"errors"
"strings"
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/entities"
"gitea.hostxtra.co.uk/mrhid6/notely/backend/internal/domain/repositories"
"go.mongodb.org/mongo-driver/v2/bson"
)
const adminGroupName = "Admin"
// PermissionService resolves and checks user permissions.
type PermissionService struct {
userRepo repositories.UserRepository
groupRepo repositories.GroupRepository
membershipRepo repositories.MembershipRepository
spaceRepo repositories.SpaceRepository
}
// NewPermissionService creates a permission service.
func NewPermissionService(
userRepo repositories.UserRepository,
groupRepo repositories.GroupRepository,
membershipRepo repositories.MembershipRepository,
spaceRepo repositories.SpaceRepository,
) *PermissionService {
return &PermissionService{
userRepo: userRepo,
groupRepo: groupRepo,
membershipRepo: membershipRepo,
spaceRepo: spaceRepo,
}
}
// EnsureAdminGroup ensures the built-in Admin group exists with full wildcard access.
func (s *PermissionService) EnsureAdminGroup(ctx context.Context) error {
adminGroup, err := s.groupRepo.GetGroupByName(ctx, adminGroupName)
if err != nil {
adminGroup = &entities.PermissionGroup{
Name: adminGroupName,
Description: "System group with full access",
Permissions: []string{"*"},
IsSystem: true,
}
if createErr := s.groupRepo.CreateGroup(ctx, adminGroup); createErr != nil {
return createErr
}
}
return nil
}
// GetUserEffectivePermissions returns a deduplicated list of permissions for a user.
func (s *PermissionService) GetUserEffectivePermissions(ctx context.Context, user *entities.User) ([]string, error) {
granted := make(map[string]struct{})
groups, err := s.groupRepo.GetGroupsByIDs(ctx, user.GroupIDs)
if err != nil {
return nil, err
}
for _, group := range groups {
for _, permission := range group.Permissions {
normalized := entities.NormalizePermission(permission)
if normalized != "" {
granted[normalized] = struct{}{}
}
}
}
result := make([]string, 0, len(granted))
for permission := range granted {
result = append(result, permission)
}
return result, nil
}
// UpdateUserEffectivePermissions resolves and persists effective user permissions.
func (s *PermissionService) UpdateUserEffectivePermissions(ctx context.Context, user *entities.User) error {
permissions, err := s.GetUserEffectivePermissions(ctx, user)
if err != nil {
return err
}
user.Permissions = permissions
return s.userRepo.UpdateUser(ctx, user)
}
// SetUserGroups assigns groups to a user and refreshes permissions.
func (s *PermissionService) SetUserGroups(ctx context.Context, userID bson.ObjectID, groupIDs []bson.ObjectID) (*entities.User, error) {
user, err := s.userRepo.GetUserByID(ctx, userID)
if err != nil {
return nil, err
}
if len(groupIDs) > 0 {
groups, err := s.groupRepo.GetGroupsByIDs(ctx, groupIDs)
if err != nil {
return nil, err
}
if len(groups) != len(groupIDs) {
return nil, errors.New("one or more groups not found")
}
}
user.GroupIDs = dedupeObjectIDs(groupIDs)
if err := s.UpdateUserEffectivePermissions(ctx, user); err != nil {
return nil, err
}
return user, nil
}
// UserHasPermission checks if user has a concrete permission, supporting wildcards.
func (s *PermissionService) UserHasPermission(ctx context.Context, userID bson.ObjectID, permission string) (bool, error) {
user, err := s.userRepo.GetUserByID(ctx, userID)
if err != nil {
return false, err
}
return s.UserEntityHasPermission(ctx, user, permission)
}
// UserEntityHasPermission checks permission from a loaded user entity.
func (s *PermissionService) UserEntityHasPermission(ctx context.Context, user *entities.User, permission string) (bool, error) {
permission = entities.NormalizePermission(permission)
if permission == "" {
return false, nil
}
granted, err := s.GetUserEffectivePermissions(ctx, user)
if err != nil {
return false, err
}
for _, pattern := range granted {
if entities.PermissionMatches(pattern, permission) {
return true, nil
}
}
return false, nil
}
// HasSpacePermission checks a space-scoped permission action, like note.create.
func (s *PermissionService) HasSpacePermission(ctx context.Context, userID, spaceID bson.ObjectID, action string) (bool, error) {
space, err := s.spaceRepo.GetSpaceByID(ctx, spaceID)
if err != nil {
return false, err
}
action = strings.Trim(strings.ToLower(action), ". ")
if action == "" {
return false, nil
}
permission := "space." + entities.SpacePermissionToken(space.Name) + "." + action
return s.UserHasPermission(ctx, userID, permission)
}
func dedupeObjectIDs(ids []bson.ObjectID) []bson.ObjectID {
seen := map[bson.ObjectID]struct{}{}
result := make([]bson.ObjectID, 0, len(ids))
for _, id := range ids {
if id.IsZero() {
continue
}
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}