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 }