service backend
This commit is contained in:
256
api/pkg/auth/internal/native/enforcer.go
Normal file
256
api/pkg/auth/internal/native/enforcer.go
Normal file
@@ -0,0 +1,256 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user