first commit
This commit is contained in:
174
backend/internal/application/services/permission_service.go
Normal file
174
backend/internal/application/services/permission_service.go
Normal file
@@ -0,0 +1,174 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user