package native import ( "context" "errors" "github.com/tech/sendico/pkg/auth/internal/native/nstructures" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" ) type Enforcer struct { logger mlogger.Logger pdb PoliciesDB rdb RolesDB } func NewEnforcer( logger mlogger.Logger, db *mongo.Database, ) (*Enforcer, error) { e := &Enforcer{logger: logger.Named("enforcer")} var err error if e.pdb, err = NewPoliciesDBDB(e.logger, db); err != nil { e.logger.Warn("Failed to create permission assignments database", zap.Error(err)) return nil, err } if e.rdb, err = NewRolesDB(e.logger, db); err != nil { e.logger.Warn("Failed to create role assignments database", zap.Error(err)) return nil, err } logger.Info("Native enforcer created") return e, nil } // Enforce checks if a user has the specified action permission on an object within a domain. func (n *Enforcer) Enforce( ctx context.Context, permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID, action model.Action, ) (bool, error) { roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef) if errors.Is(err, merrors.ErrNoData) { n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef)) return false, nil } if err != nil { n.logger.Warn("Failed to fetch roles while checking permissions", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), mzap.ObjRef("object", objectRef), zap.String("action", string(action))) return false, err } if len(roleAssignments) == 0 { n.logger.Warn("No roles found for account", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) return false, merrors.Internal("No roles found for account " + accountRef.Hex()) } allowFound := false // Track if any allow is found across roles for _, roleAssignment := range roleAssignments { policies, err := n.pdb.PoliciesForPermissionAction(ctx, roleAssignment.DescriptionRef, permissionRef, action) if err != nil && !errors.Is(err, merrors.ErrNoData) { n.logger.Warn("Failed to fetch permissions", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) return false, err } for _, permission := range policies { if permission.Effect.Effect == model.EffectDeny { n.logger.Debug("Found denying policy", mzap.ObjRef("account", accountRef), mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) return false, nil // Deny takes precedence immediately } if permission.Effect.Effect == model.EffectAllow { n.logger.Debug("Allowing policy found", mzap.ObjRef("account", accountRef), mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) allowFound = true // At least one allow found } else { n.logger.Warn("Corrupted policy", mzap.StorableRef(&permission)) return false, merrors.Internal("Corrupted action effect data for permissions entry " + permission.ID.Hex() + ": " + string(permission.Effect.Effect)) } } } // Final decision based on whether any allow was found if allowFound { return true, nil // At least one allow and no deny } n.logger.Debug("No allowing policy found", mzap.ObjRef("account", accountRef), mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) return false, nil // No allow found, default deny } // EnforceBatch checks a user’s permission for multiple objects at once. // It returns a map from objectRef -> boolean indicating whether access is granted. func (n *Enforcer) EnforceBatch( ctx context.Context, objectRefs []model.PermissionBoundStorable, accountRef primitive.ObjectID, action model.Action, ) (map[primitive.ObjectID]bool, error) { results := make(map[primitive.ObjectID]bool, len(objectRefs)) // Group objectRefs by organizationRef. objectsByVenue := make(map[primitive.ObjectID][]model.PermissionBoundStorable) for _, obj := range objectRefs { organizationRef := obj.GetOrganizationRef() objectsByVenue[organizationRef] = append(objectsByVenue[organizationRef], obj) } // Process each venue group separately. for organizationRef, objs := range objectsByVenue { // 1. Fetch roles once for this account and venue. roles, err := n.rdb.Roles(ctx, accountRef, organizationRef) if err != nil { if errors.Is(err, merrors.ErrNoData) { n.logger.Debug("No roles defined for account", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) // With no roles, mark all objects in this venue as denied. for _, obj := range objs { results[*obj.GetID()] = false } // Continue to next venue continue } n.logger.Warn("Failed to fetch roles", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) return nil, err } // 2. Extract role description references var roleRefs []primitive.ObjectID for _, role := range roles { roleRefs = append(roleRefs, role.DescriptionRef) } // 3. Fetch all policies for these roles and the given action in one call. allPolicies, err := n.pdb.PoliciesForRoles(ctx, roleRefs, action) if err != nil { n.logger.Warn("Failed to fetch policies", zap.Error(err)) return nil, err } // 4. Build a lookup map keyed by PermissionRef. policyMap := make(map[primitive.ObjectID][]nstructures.PolicyAssignment) for _, policy := range allPolicies { policyMap[policy.DescriptionRef] = append(policyMap[policy.DescriptionRef], policy) } // 5. Evaluate permissions for each object in this venue group. for _, obj := range objs { permRef := obj.GetPermissionRef() allow := false if policies, ok := policyMap[permRef]; ok { for _, policy := range policies { // Deny takes precedence. if policy.Effect.Effect == model.EffectDeny { allow = false break } if policy.Effect.Effect == model.EffectAllow { allow = true // Continue checking in case a deny exists among policies. } else { // should never get here return nil, merrors.Internal("Corrupted permissions effect in policy assignment '" + policy.GetID().Hex() + "': " + string(policy.Effect.Effect)) } } } results[*obj.GetID()] = allow } } return results, nil } // GetRoles retrieves all roles assigned to the user within the domain. func (n *Enforcer) GetRoles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]model.Role, error) { n.logger.Debug("Fetching roles for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) ra, err := n.rdb.Roles(ctx, accountRef, organizationRef) if errors.Is(err, merrors.ErrNoData) { n.logger.Debug("No roles assigned to user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) return []model.Role{}, nil } if err != nil { n.logger.Warn("Failed to fetch roles", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) return nil, err } roles := make([]model.Role, len(ra)) for i, roleAssignement := range ra { roles[i] = roleAssignement.Role } n.logger.Debug("Fetched roles", zap.Int("roles_count", len(roles))) return roles, nil } func (n *Enforcer) Reload() error { n.logger.Info("Policies reloaded") // do nothing actually return nil } // GetPermissions retrieves all effective policies for the user within the domain. func (n *Enforcer) GetPermissions(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]model.Role, []model.Permission, error) { n.logger.Debug("Fetching policies for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) roles, err := n.GetRoles(ctx, accountRef, organizationRef) if err != nil { n.logger.Warn("Failed to get roles", zap.Error(err)) return nil, nil, err } uniquePermissions := make(map[primitive.ObjectID]model.Permission) for _, role := range roles { perms, err := n.pdb.PoliciesForRole(ctx, role.DescriptionRef) if err != nil { n.logger.Warn("Failed to get policies for role", zap.Error(err), mzap.ObjRef("role_ref", role.DescriptionRef)) continue } n.logger.Debug("Policies fetched for role", mzap.ObjRef("role_ref", role.DescriptionRef), zap.Int("count", len(perms))) for _, p := range perms { uniquePermissions[*p.GetID()] = model.Permission{ RolePolicy: model.RolePolicy{ Policy: p.Policy, RoleDescriptionRef: p.RoleRef, }, AccountRef: accountRef, } } } permissionsSlice := make([]model.Permission, 0, len(uniquePermissions)) for _, permission := range uniquePermissions { permissionsSlice = append(permissionsSlice, permission) } n.logger.Debug("Policies fetched successfully", zap.Int("count", len(permissionsSlice))) return roles, permissionsSlice, nil }