Files
sendico/api/pkg/auth/internal/native/enforcer.go
Stephan D 62a6631b9a
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
service backend
2025-11-07 18:35:26 +01:00

257 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 users 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
}