Some checks failed
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/nats Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/bump_version unknown status
ci/woodpecker/push/chain_gateway Pipeline failed
263 lines
10 KiB
Go
263 lines
10 KiB
Go
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) {
|
||
if organizationRef.IsZero() {
|
||
n.logger.Warn("Missing organization context", 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, merrors.InvalidArgument("organization context missing", "organizationRef")
|
||
}
|
||
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
|
||
}
|