175 lines
4.9 KiB
Go
175 lines
4.9 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/noteapp/backend/internal/domain/entities"
|
|
"github.com/noteapp/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
|
|
}
|