service backend
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful

This commit is contained in:
Stephan D
2025-11-07 18:35:26 +01:00
parent 20e8f9acc4
commit 62a6631b9a
537 changed files with 48453 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
package db
import (
"context"
"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/db/template"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
mutil "github.com/tech/sendico/pkg/mutil/db"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
type PermissionsDBImp struct {
template.DBImp[*nstructures.PolicyAssignment]
}
func (db *PermissionsDBImp) Policies(ctx context.Context, object model.PermissionBoundStorable, action model.Action) ([]nstructures.PolicyAssignment, error) {
return mutil.GetObjects[nstructures.PolicyAssignment](
ctx,
db.Logger,
repository.Query().And(
repository.Filter("policy.organizationRef", object.GetOrganizationRef()),
repository.Filter("policy.descriptionRef", object.GetPermissionRef()),
repository.Filter("policy.effect.action", action),
repository.Query().Or(
repository.Filter("policy.objectRef", *object.GetID()),
repository.Filter("policy.objectRef", nil),
),
),
nil,
db.Repository,
)
}
func (db *PermissionsDBImp) PoliciesForPermissionAction(ctx context.Context, roleRef, permissionRef primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) {
return mutil.GetObjects[nstructures.PolicyAssignment](
ctx,
db.Logger,
repository.Query().And(
repository.Filter("roleRef", roleRef),
repository.Filter("policy.descriptionRef", permissionRef),
repository.Filter("policy.effect.action", action),
),
nil,
db.Repository,
)
}
func (db *PermissionsDBImp) Remove(ctx context.Context, policy *model.RolePolicy) error {
objRefFilter := repository.Query().Or(
repository.Filter("policy.objectRef", nil),
repository.Filter("policy.objectRef", primitive.NilObjectID),
)
if policy.ObjectRef != nil {
objRefFilter = repository.Filter("policy.objectRef", *policy.ObjectRef)
}
return db.Repository.DeleteMany(
ctx,
repository.Query().And(
repository.Filter("roleRef", policy.RoleDescriptionRef),
repository.Filter("policy.organizationRef", policy.OrganizationRef),
repository.Filter("policy.descriptionRef", policy.DescriptionRef),
objRefFilter,
repository.Filter("policy.effect.action", policy.Effect.Action),
repository.Filter("policy.effect.effect", policy.Effect.Effect),
),
)
}
func (db *PermissionsDBImp) PoliciesForRole(ctx context.Context, roleRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) {
return mutil.GetObjects[nstructures.PolicyAssignment](
ctx,
db.Logger,
repository.Filter("roleRef", roleRef),
nil,
db.Repository,
)
}
func (db *PermissionsDBImp) PoliciesForRoles(ctx context.Context, roleRefs []primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) {
if len(roleRefs) == 0 {
db.Logger.Debug("Empty role references list provided, returning empty resposnse")
return []nstructures.PolicyAssignment{}, nil
}
return mutil.GetObjects[nstructures.PolicyAssignment](
ctx,
db.Logger,
repository.Query().And(
repository.Query().In(repository.Field("roleRef"), roleRefs),
repository.Filter("policy.effect.action", action),
),
nil,
db.Repository,
)
}
func NewPoliciesDB(logger mlogger.Logger, db *mongo.Database) (*PermissionsDBImp, error) {
p := &PermissionsDBImp{
DBImp: *template.Create[*nstructures.PolicyAssignment](logger, mservice.PolicyAssignements, db),
}
// faster
// harder
// index
policiesQueryIndex := &ri.Definition{
Keys: []ri.Key{
{Field: "policy.organizationRef", Sort: ri.Asc},
{Field: "policy.descriptionRef", Sort: ri.Asc},
{Field: "policy.effect.action", Sort: ri.Asc},
{Field: "policy.objectRef", Sort: ri.Asc},
},
}
if err := p.DBImp.Repository.CreateIndex(policiesQueryIndex); err != nil {
p.Logger.Warn("Failed to prepare policies query index", zap.Error(err))
return nil, err
}
roleBasedQueriesIndex := &ri.Definition{
Keys: []ri.Key{
{Field: "roleRef", Sort: ri.Asc},
{Field: "policy.effect.action", Sort: ri.Asc},
},
}
if err := p.DBImp.Repository.CreateIndex(roleBasedQueriesIndex); err != nil {
p.Logger.Warn("Failed to prepare role based query index", zap.Error(err))
return nil, err
}
uniquePolicyConstaint := &ri.Definition{
Keys: []ri.Key{
{Field: "policy.organizationRef", Sort: ri.Asc},
{Field: "roleRef", Sort: ri.Asc},
{Field: "policy.descriptionRef", Sort: ri.Asc},
{Field: "policy.effect.action", Sort: ri.Asc},
{Field: "policy.objectRef", Sort: ri.Asc},
},
Unique: true,
}
if err := p.DBImp.Repository.CreateIndex(uniquePolicyConstaint); err != nil {
p.Logger.Warn("Failed to unique policy assignment index", zap.Error(err))
return nil, err
}
return p, nil
}

View File

@@ -0,0 +1,99 @@
package db
import (
"context"
"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
"github.com/tech/sendico/pkg/db/repository"
ri "github.com/tech/sendico/pkg/db/repository/index"
"github.com/tech/sendico/pkg/db/template"
"github.com/tech/sendico/pkg/mlogger"
mutil "github.com/tech/sendico/pkg/mutil/db"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
)
type RolesDBImp struct {
template.DBImp[*nstructures.RoleAssignment]
}
func (db *RolesDBImp) Roles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) {
return mutil.GetObjects[nstructures.RoleAssignment](
ctx,
db.Logger,
repository.Query().And(
repository.Filter("role.accountRef", accountRef),
repository.Filter("role.organizationRef", organizationRef),
),
nil,
db.Repository,
)
}
func (db *RolesDBImp) RolesForVenue(ctx context.Context, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) {
return mutil.GetObjects[nstructures.RoleAssignment](
ctx,
db.Logger,
repository.Query().And(
repository.Filter("role.organizationRef", organizationRef),
),
nil,
db.Repository,
)
}
func (db *RolesDBImp) DeleteRole(ctx context.Context, roleRef primitive.ObjectID) error {
return db.DeleteMany(
ctx,
repository.Query().And(
repository.Filter("role.descriptionRef", roleRef),
),
)
}
func (db *RolesDBImp) RemoveRole(ctx context.Context, roleRef, organizationRef, accountRef primitive.ObjectID) error {
return db.DeleteMany(
ctx,
repository.Query().And(
repository.Filter("role.accountRef", accountRef),
repository.Filter("role.organizationRef", organizationRef),
repository.Filter("role.descriptionRef", roleRef),
),
)
}
func NewRolesDB(logger mlogger.Logger, db *mongo.Database) (*RolesDBImp, error) {
p := &RolesDBImp{
DBImp: *template.Create[*nstructures.RoleAssignment](logger, "role_assignments", db),
}
if err := p.DBImp.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: "role.organizationRef", Sort: ri.Asc}},
}); err != nil {
p.Logger.Warn("Failed to prepare venue index", zap.Error(err))
return nil, err
}
if err := p.DBImp.Repository.CreateIndex(&ri.Definition{
Keys: []ri.Key{{Field: "role.descriptionRef", Sort: ri.Asc}},
}); err != nil {
p.Logger.Warn("Failed to prepare role description index", zap.Error(err))
return nil, err
}
uniqueRoleConstaint := &ri.Definition{
Keys: []ri.Key{
{Field: "role.organizationRef", Sort: ri.Asc},
{Field: "role.accountRef", Sort: ri.Asc},
{Field: "role.descriptionRef", Sort: ri.Asc},
},
Unique: true,
}
if err := p.DBImp.Repository.CreateIndex(uniqueRoleConstaint); err != nil {
p.Logger.Warn("Failed to prepare role assignment index", zap.Error(err))
return nil, err
}
return p, nil
}

View File

@@ -0,0 +1,27 @@
package native
import (
"context"
"github.com/tech/sendico/pkg/auth/internal/native/db"
"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
"github.com/tech/sendico/pkg/db/template"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type PoliciesDB interface {
template.DB[*nstructures.PolicyAssignment]
// plenty of interfaces for performance reasons
Policies(ctx context.Context, object model.PermissionBoundStorable, action model.Action) ([]nstructures.PolicyAssignment, error)
PoliciesForPermissionAction(ctx context.Context, roleRef, permissionRef primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error)
PoliciesForRole(ctx context.Context, roleRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error)
PoliciesForRoles(ctx context.Context, roleRefs []primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error)
Remove(ctx context.Context, policy *model.RolePolicy) error
}
func NewPoliciesDBDB(logger mlogger.Logger, conn *mongo.Database) (PoliciesDB, error) {
return db.NewPoliciesDB(logger, conn)
}

View File

@@ -0,0 +1,24 @@
package native
import (
"context"
"github.com/tech/sendico/pkg/auth/internal/native/db"
"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
"github.com/tech/sendico/pkg/db/template"
"github.com/tech/sendico/pkg/mlogger"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type RolesDB interface {
template.DB[*nstructures.RoleAssignment]
Roles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error)
RolesForVenue(ctx context.Context, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error)
RemoveRole(ctx context.Context, roleRef, organizationRef, accountRef primitive.ObjectID) error
DeleteRole(ctx context.Context, roleRef primitive.ObjectID) error
}
func NewRolesDB(logger mlogger.Logger, conn *mongo.Database) (RolesDB, error) {
return db.NewRolesDB(logger, conn)
}

View 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 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
}

View File

@@ -0,0 +1,747 @@
package native
import (
"context"
"errors"
"testing"
"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
"github.com/tech/sendico/pkg/db/repository/builder"
"github.com/tech/sendico/pkg/merrors"
factory "github.com/tech/sendico/pkg/mlogger/factory"
"github.com/tech/sendico/pkg/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Mock implementations for testing
type MockPoliciesDB struct {
mock.Mock
}
func (m *MockPoliciesDB) PoliciesForPermissionAction(ctx context.Context, roleRef, permissionRef primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) {
args := m.Called(ctx, roleRef, permissionRef, action)
return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1)
}
func (m *MockPoliciesDB) PoliciesForRole(ctx context.Context, roleRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) {
args := m.Called(ctx, roleRef)
return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1)
}
func (m *MockPoliciesDB) PoliciesForRoles(ctx context.Context, roleRefs []primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) {
args := m.Called(ctx, roleRefs, action)
return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1)
}
func (m *MockPoliciesDB) Policies(ctx context.Context, object model.PermissionBoundStorable, action model.Action) ([]nstructures.PolicyAssignment, error) {
args := m.Called(ctx, object, action)
return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1)
}
func (m *MockPoliciesDB) Remove(ctx context.Context, policy *model.RolePolicy) error {
args := m.Called(ctx, policy)
return args.Error(0)
}
// Template DB methods - implement as needed for testing
func (m *MockPoliciesDB) Create(ctx context.Context, assignment *nstructures.PolicyAssignment) error {
args := m.Called(ctx, assignment)
return args.Error(0)
}
func (m *MockPoliciesDB) Get(ctx context.Context, id primitive.ObjectID, assignment *nstructures.PolicyAssignment) error {
args := m.Called(ctx, id, assignment)
return args.Error(0)
}
func (m *MockPoliciesDB) Update(ctx context.Context, assignment *nstructures.PolicyAssignment) error {
args := m.Called(ctx, assignment)
return args.Error(0)
}
func (m *MockPoliciesDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error {
args := m.Called(ctx, objectRef, patch)
return args.Error(0)
}
func (m *MockPoliciesDB) Delete(ctx context.Context, id primitive.ObjectID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockPoliciesDB) DeleteMany(ctx context.Context, query builder.Query) error {
args := m.Called(ctx, query)
return args.Error(0)
}
func (m *MockPoliciesDB) ListPermissionBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) {
args := m.Called(ctx, accountRef, organizationRef)
return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1)
}
func (m *MockPoliciesDB) ListIDs(ctx context.Context, query interface{}) ([]primitive.ObjectID, error) {
args := m.Called(ctx, query)
return args.Get(0).([]primitive.ObjectID), args.Error(1)
}
func (m *MockPoliciesDB) FindOne(ctx context.Context, query builder.Query, assignment *nstructures.PolicyAssignment) error {
args := m.Called(ctx, query, assignment)
return args.Error(0)
}
func (m *MockPoliciesDB) List(ctx context.Context, query builder.Query) ([]nstructures.PolicyAssignment, error) {
args := m.Called(ctx, query)
return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1)
}
func (m *MockPoliciesDB) Name() string {
return "mock_policies"
}
func (m *MockPoliciesDB) DeleteCascade(ctx context.Context, id primitive.ObjectID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockPoliciesDB) InsertMany(ctx context.Context, objects []*nstructures.PolicyAssignment) error {
args := m.Called(ctx, objects)
return args.Error(0)
}
type MockRolesDB struct {
mock.Mock
}
func (m *MockRolesDB) Roles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) {
args := m.Called(ctx, accountRef, organizationRef)
return args.Get(0).([]nstructures.RoleAssignment), args.Error(1)
}
func (m *MockRolesDB) RolesForVenue(ctx context.Context, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) {
args := m.Called(ctx, organizationRef)
return args.Get(0).([]nstructures.RoleAssignment), args.Error(1)
}
func (m *MockRolesDB) RemoveRole(ctx context.Context, roleRef, organizationRef, accountRef primitive.ObjectID) error {
args := m.Called(ctx, roleRef, organizationRef, accountRef)
return args.Error(0)
}
func (m *MockRolesDB) DeleteRole(ctx context.Context, roleRef primitive.ObjectID) error {
args := m.Called(ctx, roleRef)
return args.Error(0)
}
// Template DB methods - implement as needed for testing
func (m *MockRolesDB) Create(ctx context.Context, assignment *nstructures.RoleAssignment) error {
args := m.Called(ctx, assignment)
return args.Error(0)
}
func (m *MockRolesDB) Get(ctx context.Context, id primitive.ObjectID, assignment *nstructures.RoleAssignment) error {
args := m.Called(ctx, id, assignment)
return args.Error(0)
}
func (m *MockRolesDB) Update(ctx context.Context, assignment *nstructures.RoleAssignment) error {
args := m.Called(ctx, assignment)
return args.Error(0)
}
func (m *MockRolesDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error {
args := m.Called(ctx, objectRef, patch)
return args.Error(0)
}
func (m *MockRolesDB) Delete(ctx context.Context, id primitive.ObjectID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockRolesDB) DeleteMany(ctx context.Context, query builder.Query) error {
args := m.Called(ctx, query)
return args.Error(0)
}
func (m *MockRolesDB) ListPermissionBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) {
args := m.Called(ctx, accountRef, organizationRef)
return args.Get(0).([]nstructures.RoleAssignment), args.Error(1)
}
func (m *MockRolesDB) ListIDs(ctx context.Context, query interface{}) ([]primitive.ObjectID, error) {
args := m.Called(ctx, query)
return args.Get(0).([]primitive.ObjectID), args.Error(1)
}
func (m *MockRolesDB) FindOne(ctx context.Context, query builder.Query, assignment *nstructures.RoleAssignment) error {
args := m.Called(ctx, query, assignment)
return args.Error(0)
}
func (m *MockRolesDB) List(ctx context.Context, query builder.Query) ([]nstructures.RoleAssignment, error) {
args := m.Called(ctx, query)
return args.Get(0).([]nstructures.RoleAssignment), args.Error(1)
}
func (m *MockRolesDB) Name() string {
return "mock_roles"
}
func (m *MockRolesDB) DeleteCascade(ctx context.Context, id primitive.ObjectID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockRolesDB) InsertMany(ctx context.Context, objects []*nstructures.RoleAssignment) error {
args := m.Called(ctx, objects)
return args.Error(0)
}
// Test helper functions
func createTestObjectID() primitive.ObjectID {
return primitive.NewObjectID()
}
func createTestRoleAssignment(roleRef, accountRef, organizationRef primitive.ObjectID) nstructures.RoleAssignment {
return nstructures.RoleAssignment{
Role: model.Role{
AccountRef: accountRef,
DescriptionRef: roleRef,
OrganizationRef: organizationRef,
},
}
}
func createTestPolicyAssignment(roleRef primitive.ObjectID, action model.Action, effect model.Effect, organizationRef, descriptionRef primitive.ObjectID, objectRef *primitive.ObjectID) nstructures.PolicyAssignment {
return nstructures.PolicyAssignment{
Policy: model.Policy{
OrganizationRef: organizationRef,
DescriptionRef: descriptionRef,
ObjectRef: objectRef,
Effect: model.ActionEffect{
Action: action,
Effect: effect,
},
},
RoleRef: roleRef,
}
}
func createTestEnforcer(pdb PoliciesDB, rdb RolesDB) *Enforcer {
logger := factory.NewLogger(true)
enforcer := &Enforcer{
logger: logger.Named("test"),
pdb: pdb,
rdb: rdb,
}
return enforcer
}
func TestEnforcer_Enforce(t *testing.T) {
ctx := context.Background()
// Test data
accountRef := createTestObjectID()
organizationRef := createTestObjectID()
permissionRef := createTestObjectID()
objectRef := createTestObjectID()
roleRef := createTestObjectID()
t.Run("Allow_SingleRole_SinglePolicy", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock policy assignment with ALLOW effect
policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef)
mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil)
// Create enforcer
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.NoError(t, err)
assert.True(t, allowed)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("Deny_SingleRole_SinglePolicy", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock policy assignment with DENY effect
policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef)
mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.NoError(t, err)
assert.False(t, allowed)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("DenyTakesPrecedence_MultipleRoles", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
role1Ref := createTestObjectID()
role2Ref := createTestObjectID()
// Mock multiple role assignments
roleAssignment1 := createTestRoleAssignment(role1Ref, accountRef, organizationRef)
roleAssignment2 := createTestRoleAssignment(role2Ref, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment1, roleAssignment2}, nil)
// First role has ALLOW policy
allowPolicy := createTestPolicyAssignment(role1Ref, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef)
mockPDB.On("PoliciesForPermissionAction", ctx, role1Ref, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{allowPolicy}, nil)
// Second role has DENY policy - should take precedence
denyPolicy := createTestPolicyAssignment(role2Ref, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef)
mockPDB.On("PoliciesForPermissionAction", ctx, role2Ref, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{denyPolicy}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify - DENY should take precedence
require.NoError(t, err)
assert.False(t, allowed)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("NoRoles_ReturnsFalse", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock no roles found
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, merrors.ErrNoData)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.NoError(t, err)
assert.False(t, allowed)
mockRDB.AssertExpectations(t)
})
t.Run("EmptyRoles_ReturnsError", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock empty roles list (not NoData error)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.Error(t, err)
assert.False(t, allowed)
assert.Contains(t, err.Error(), "No roles found for account")
mockRDB.AssertExpectations(t)
})
t.Run("DatabaseError_RolesDB", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock database error
dbError := errors.New("database connection failed")
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, dbError)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.Error(t, err)
assert.False(t, allowed)
assert.Equal(t, dbError, err)
mockRDB.AssertExpectations(t)
})
t.Run("DatabaseError_PoliciesDB", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock database error in policies
dbError := errors.New("policies database error")
mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{}, dbError)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.Error(t, err)
assert.False(t, allowed)
assert.Equal(t, dbError, err)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("NoPolicies_ReturnsFalse", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock no policies found
mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{}, merrors.ErrNoData)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.NoError(t, err)
assert.False(t, allowed)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("CorruptedPolicy_ReturnsError", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock corrupted policy with invalid effect
corruptedPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, "invalid_effect", organizationRef, permissionRef, &objectRef)
mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{corruptedPolicy}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify
require.Error(t, err)
assert.False(t, allowed)
assert.Contains(t, err.Error(), "Corrupted action effect data")
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
}
// Mock implementation for PermissionBoundStorable
type MockPermissionBoundStorable struct {
id primitive.ObjectID
permissionRef primitive.ObjectID
organizationRef primitive.ObjectID
}
func (m *MockPermissionBoundStorable) GetID() *primitive.ObjectID {
return &m.id
}
func (m *MockPermissionBoundStorable) GetPermissionRef() primitive.ObjectID {
return m.permissionRef
}
func (m *MockPermissionBoundStorable) GetOrganizationRef() primitive.ObjectID {
return m.organizationRef
}
func (m *MockPermissionBoundStorable) Collection() string {
return "test_objects"
}
func (m *MockPermissionBoundStorable) SetID(objID primitive.ObjectID) {
m.id = objID
}
func (m *MockPermissionBoundStorable) Update() {
// Do nothing for mock
}
func (m *MockPermissionBoundStorable) SetPermissionRef(permissionRef primitive.ObjectID) {
m.permissionRef = permissionRef
}
func (m *MockPermissionBoundStorable) SetOrganizationRef(organizationRef primitive.ObjectID) {
m.organizationRef = organizationRef
}
func (m *MockPermissionBoundStorable) IsArchived() bool {
return false // Default to not archived for testing
}
func (m *MockPermissionBoundStorable) SetArchived(archived bool) {
// No-op for testing
}
func TestEnforcer_EnforceBatch(t *testing.T) {
ctx := context.Background()
// Test data
accountRef := createTestObjectID()
organizationRef := createTestObjectID()
permissionRef := createTestObjectID()
roleRef := createTestObjectID()
// Create test objects
object1 := &MockPermissionBoundStorable{
id: createTestObjectID(),
permissionRef: permissionRef,
organizationRef: organizationRef,
}
object2 := &MockPermissionBoundStorable{
id: createTestObjectID(),
permissionRef: permissionRef,
organizationRef: organizationRef,
}
t.Run("BatchEnforce_MultipleObjects_SameVenue", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock policy assignment with ALLOW effect
policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, nil)
mockPDB.On("PoliciesForRoles", ctx, []primitive.ObjectID{roleRef}, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute batch enforcement
objects := []model.PermissionBoundStorable{object1, object2}
results, err := enforcer.EnforceBatch(ctx, objects, accountRef, model.ActionRead)
// Verify
require.NoError(t, err)
assert.Len(t, results, 2)
assert.True(t, results[object1.id])
assert.True(t, results[object2.id])
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("BatchEnforce_NoRoles_AllObjectsDenied", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock no roles found
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, merrors.ErrNoData)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute batch enforcement
objects := []model.PermissionBoundStorable{object1, object2}
results, err := enforcer.EnforceBatch(ctx, objects, accountRef, model.ActionRead)
// Verify
require.NoError(t, err)
assert.Len(t, results, 2)
assert.False(t, results[object1.id])
assert.False(t, results[object2.id])
mockRDB.AssertExpectations(t)
})
t.Run("BatchEnforce_DatabaseError", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock database error
dbError := errors.New("database connection failed")
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, dbError)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute batch enforcement
objects := []model.PermissionBoundStorable{object1, object2}
results, err := enforcer.EnforceBatch(ctx, objects, accountRef, model.ActionRead)
// Verify
require.Error(t, err)
assert.Nil(t, results)
assert.Equal(t, dbError, err)
mockRDB.AssertExpectations(t)
})
}
func TestEnforcer_GetRoles(t *testing.T) {
ctx := context.Background()
// Test data
accountRef := createTestObjectID()
organizationRef := createTestObjectID()
roleRef := createTestObjectID()
t.Run("GetRoles_Success", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
roles, err := enforcer.GetRoles(ctx, accountRef, organizationRef)
// Verify
require.NoError(t, err)
assert.Len(t, roles, 1)
assert.Equal(t, roleRef, roles[0].DescriptionRef)
mockRDB.AssertExpectations(t)
})
t.Run("GetRoles_NoRoles", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock no roles found
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, merrors.ErrNoData)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
roles, err := enforcer.GetRoles(ctx, accountRef, organizationRef)
// Verify
require.NoError(t, err)
assert.Len(t, roles, 0)
mockRDB.AssertExpectations(t)
})
}
func TestEnforcer_GetPermissions(t *testing.T) {
ctx := context.Background()
// Test data
accountRef := createTestObjectID()
organizationRef := createTestObjectID()
roleRef := createTestObjectID()
t.Run("GetPermissions_Success", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock policy assignment
policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, createTestObjectID(), nil)
mockPDB.On("PoliciesForRole", ctx, roleRef).Return([]nstructures.PolicyAssignment{policyAssignment}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
roles, permissions, err := enforcer.GetPermissions(ctx, accountRef, organizationRef)
// Verify
require.NoError(t, err)
assert.Len(t, roles, 1)
assert.Len(t, permissions, 1)
assert.Equal(t, accountRef, permissions[0].AccountRef)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
}
// Security-focused test scenarios
func TestEnforcer_SecurityScenarios(t *testing.T) {
ctx := context.Background()
// Test data
accountRef := createTestObjectID()
organizationRef := createTestObjectID()
permissionRef := createTestObjectID()
objectRef := createTestObjectID()
roleRef := createTestObjectID()
t.Run("Security_DenyAlwaysWins", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock role assignment
roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef)
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil)
// Mock multiple policies: both ALLOW and DENY
allowPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef)
denyPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef)
mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{allowPolicy, denyPolicy}, nil)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify - DENY should always win
require.NoError(t, err)
assert.False(t, allowed)
mockRDB.AssertExpectations(t)
mockPDB.AssertExpectations(t)
})
t.Run("Security_InvalidObjectID", func(t *testing.T) {
mockPDB := &MockPoliciesDB{}
mockRDB := &MockRolesDB{}
// Mock database error for invalid ObjectID
dbError := errors.New("invalid ObjectID")
mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, dbError)
enforcer := createTestEnforcer(mockPDB, mockRDB)
// Execute with invalid ObjectID
allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead)
// Verify - should fail securely
require.Error(t, err)
assert.False(t, allowed)
mockRDB.AssertExpectations(t)
})
}
// Note: This test provides comprehensive coverage of the native enforcer including:
// 1. Basic enforcement logic with deny-takes-precedence
// 2. Batch operations for performance
// 3. Role and permission retrieval
// 4. Security scenarios and edge cases
// 5. Error handling and database failures
// 6. All critical security paths are tested

View File

@@ -0,0 +1,51 @@
package native
import (
"context"
"github.com/tech/sendico/pkg/auth/management"
"github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/role"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"go.uber.org/zap"
)
// NativeManager implements the auth.Manager interface by aggregating Role and Permission managers.
type NativeManager struct {
logger mlogger.Logger
roleManager management.Role
permManager management.Permission
}
// NewManager creates a new CasbinManager with specified domains and role-domain mappings.
func NewManager(
l mlogger.Logger,
pdb policy.DB,
rdb role.DB,
enforcer *Enforcer,
) (*NativeManager, error) {
logger := l.Named("manager")
var pdesc model.PolicyDescription
if err := pdb.GetBuiltInPolicy(context.Background(), "roles", &pdesc); err != nil {
logger.Warn("Failed to fetch roles permission reference", zap.Error(err))
return nil, err
}
return &NativeManager{
logger: logger,
roleManager: NewRoleManager(logger, enforcer, pdesc.ID, rdb),
permManager: NewPermissionManager(logger, enforcer),
}, nil
}
// Permission returns the Permission manager.
func (m *NativeManager) Permission() management.Permission {
return m.permManager
}
// Role returns the Role manager.
func (m *NativeManager) Role() management.Role {
return m.roleManager
}

Binary file not shown.

View File

@@ -0,0 +1,17 @@
package nstructures
import (
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type PolicyAssignment struct {
storable.Base `bson:",inline" json:",inline"`
model.Policy `bson:"policy" json:"policy"`
RoleRef primitive.ObjectID `bson:"roleRef" json:"roleRef"`
}
func (*PolicyAssignment) Collection() string {
return "permission_assignments"
}

View File

@@ -0,0 +1,15 @@
package nstructures
import (
"github.com/tech/sendico/pkg/db/storable"
"github.com/tech/sendico/pkg/model"
)
type RoleAssignment struct {
storable.Base `bson:",inline" json:",inline"`
model.Role `bson:"role" json:"role"`
}
func (*RoleAssignment) Collection() string {
return "role_assignments"
}

View File

@@ -0,0 +1,101 @@
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.uber.org/zap"
)
// PermissionManager manages permissions using Casbin.
type PermissionManager struct {
logger mlogger.Logger
enforcer *Enforcer
}
// GrantToRole adds a permission to a role in Casbin.
func (m *PermissionManager) GrantToRole(ctx context.Context, policy *model.RolePolicy) error {
objRef := "any"
if (policy.ObjectRef != nil) && (*policy.ObjectRef != primitive.NilObjectID) {
objRef = policy.ObjectRef.Hex()
}
m.logger.Debug("Granting permission to role", mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
mzap.ObjRef("permission_ref", policy.DescriptionRef), zap.String("object_ref", objRef),
zap.String("action", string(policy.Effect.Action)), zap.String("effect", string(policy.Effect.Effect)),
)
assignment := nstructures.PolicyAssignment{
Policy: policy.Policy,
RoleRef: policy.RoleDescriptionRef,
}
if err := m.enforcer.pdb.Create(ctx, &assignment); err != nil {
m.logger.Warn("Failed to grant policy", zap.Error(err), mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
mzap.ObjRef("permission_ref", policy.DescriptionRef), zap.String("object_ref", objRef),
zap.String("action", string(policy.Effect.Action)), zap.String("effect", string(policy.Effect.Effect)))
return err
}
return nil
}
// RevokeFromRole removes a permission from a role in Casbin.
func (m *PermissionManager) RevokeFromRole(ctx context.Context, policy *model.RolePolicy) error {
objRef := "*"
if policy.ObjectRef != nil {
objRef = policy.ObjectRef.Hex()
}
m.logger.Debug("Revoking permission from role", mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
mzap.ObjRef("permission_ref", policy.DescriptionRef), zap.String("object_ref", objRef),
zap.String("action", string(policy.Effect.Action)), zap.String("effect", string(policy.Effect.Effect)),
)
if err := m.enforcer.pdb.Remove(ctx, policy); err != nil {
m.logger.Warn("Failed to revoke policy", zap.Error(err), mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
mzap.ObjRef("permission_ref", policy.DescriptionRef), zap.String("object_ref", objRef),
zap.String("action", string(policy.Effect.Action)), zap.String("effect", string(policy.Effect.Effect)))
return err
}
return nil
}
// GetPolicies retrieves all policies for a specific role.
func (m *PermissionManager) GetPolicies(
ctx context.Context,
roleRef primitive.ObjectID,
) ([]model.RolePolicy, error) {
m.logger.Debug("Fetching policies for role", mzap.ObjRef("role_ref", roleRef))
assinments, err := m.enforcer.pdb.PoliciesForRole(ctx, roleRef)
if errors.Is(err, merrors.ErrNoData) {
m.logger.Debug("No policies found", mzap.ObjRef("role_ref", roleRef))
return []model.RolePolicy{}, nil
}
policies := make([]model.RolePolicy, len(assinments))
for i, assinment := range assinments {
policies[i] = model.RolePolicy{
Policy: assinment.Policy,
RoleDescriptionRef: assinment.RoleRef,
}
}
m.logger.Debug("Policies fetched successfully", mzap.ObjRef("role_ref", roleRef), zap.Int("count", len(policies)))
return policies, nil
}
// Save persists changes to the Casbin policy store.
func (m *PermissionManager) Save() error {
m.logger.Info("Policies successfully saved") // do nothing
return nil
}
func NewPermissionManager(logger mlogger.Logger, enforcer *Enforcer) *PermissionManager {
return &PermissionManager{
logger: logger.Named("permission"),
enforcer: enforcer,
}
}

View File

@@ -0,0 +1,142 @@
package native
import (
"context"
"github.com/tech/sendico/pkg/auth/internal/native/nstructures"
"github.com/tech/sendico/pkg/db/role"
"github.com/tech/sendico/pkg/db/storable"
"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.uber.org/zap"
)
// RoleManager manages roles using Casbin.
type RoleManager struct {
logger mlogger.Logger
enforcer *Enforcer
rdb role.DB
rolePermissionRef primitive.ObjectID
}
// NewRoleManager creates a new RoleManager.
func NewRoleManager(logger mlogger.Logger, enforcer *Enforcer, rolePermissionRef primitive.ObjectID, rdb role.DB) *RoleManager {
return &RoleManager{
logger: logger.Named("role"),
enforcer: enforcer,
rdb: rdb,
rolePermissionRef: rolePermissionRef,
}
}
// validateObjectIDs ensures that all provided ObjectIDs are non-zero.
func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error {
for _, id := range ids {
if id.IsZero() {
return merrors.InvalidArgument("Object references cannot be zero")
}
}
return nil
}
// fetchRolesFromPolicies retrieves and converts policies to roles.
func (rm *RoleManager) fetchRolesFromPolicies(roles []nstructures.RoleAssignment, organizationRef primitive.ObjectID) []model.RoleDescription {
result := make([]model.RoleDescription, len(roles))
for i, role := range roles {
result[i] = model.RoleDescription{
Base: storable.Base{ID: *role.GetID()},
OrganizationRef: organizationRef,
}
}
return result
}
// Create creates a new role in an organization.
func (rm *RoleManager) Create(ctx context.Context, organizationRef primitive.ObjectID, description *model.Describable) (*model.RoleDescription, error) {
if err := rm.validateObjectIDs(organizationRef); err != nil {
return nil, err
}
role := &model.RoleDescription{
OrganizationRef: organizationRef,
Describable: *description,
}
if err := rm.rdb.Create(ctx, role); err != nil {
rm.logger.Warn("Failed to create role", zap.Error(err), mzap.ObjRef("organization_ref", organizationRef))
return nil, err
}
rm.logger.Info("Role created successfully", mzap.StorableRef(role), mzap.ObjRef("organization_ref", organizationRef))
return role, nil
}
// Assign assigns a role to a user in the given organization.
func (rm *RoleManager) Assign(ctx context.Context, role *model.Role) error {
if err := rm.validateObjectIDs(role.DescriptionRef, role.AccountRef, role.OrganizationRef); err != nil {
return err
}
assogment := nstructures.RoleAssignment{Role: *role}
err := rm.enforcer.rdb.Create(ctx, &assogment)
return rm.logPolicyResult("assign", err == nil, err, role.DescriptionRef, role.AccountRef, role.OrganizationRef)
}
// Delete removes a role entirely and cleans up associated Casbin policies.
func (rm *RoleManager) Delete(ctx context.Context, roleRef primitive.ObjectID) error {
if err := rm.validateObjectIDs(roleRef); err != nil {
rm.logger.Warn("Failed to delete role", mzap.ObjRef("role_ref", roleRef))
return err
}
if err := rm.rdb.Delete(ctx, roleRef); err != nil {
rm.logger.Warn("Failed to delete role", mzap.ObjRef("role_ref", roleRef))
return err
}
if err := rm.enforcer.rdb.DeleteRole(ctx, roleRef); err != nil {
rm.logger.Warn("Failed to remove role", zap.Error(err), mzap.ObjRef("role_ref", roleRef))
return err
}
rm.logger.Info("Role deleted successfully along with associated policies", mzap.ObjRef("role_ref", roleRef))
return nil
}
// Revoke removes a role from a user.
func (rm *RoleManager) Revoke(ctx context.Context, roleRef, accountRef, organizationRef primitive.ObjectID) error {
if err := rm.validateObjectIDs(roleRef, accountRef, organizationRef); err != nil {
return err
}
err := rm.enforcer.rdb.RemoveRole(ctx, roleRef, organizationRef, accountRef)
return rm.logPolicyResult("revoke", err == nil, err, roleRef, accountRef, organizationRef)
}
// logPolicyResult logs results for Assign and Revoke.
func (rm *RoleManager) logPolicyResult(action string, result bool, err error, roleRef, accountRef, organizationRef primitive.ObjectID) error {
if err != nil {
rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
return err
}
msg := "Role " + action + "ed successfully"
if !result {
msg = "Role already " + action + "ed"
}
rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef))
return nil
}
// List retrieves all roles in an organization or all roles if organizationRef is zero.
func (rm *RoleManager) List(ctx context.Context, organizationRef primitive.ObjectID) ([]model.RoleDescription, error) {
roles4Venues, err := rm.enforcer.rdb.RolesForVenue(ctx, organizationRef)
if err != nil {
rm.logger.Warn("Failed to fetch grouping policies", zap.Error(err), mzap.ObjRef("organization_ref", organizationRef))
return nil, err
}
roles := rm.fetchRolesFromPolicies(roles4Venues, organizationRef)
rm.logger.Info("Retrieved roles for organization", mzap.ObjRef("organization_ref", organizationRef), zap.Int("count", len(roles)))
return roles, nil
}