service backend
This commit is contained in:
23
api/pkg/auth/internal/casbin/action.go
Normal file
23
api/pkg/auth/internal/casbin/action.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
func stringToAction(actionStr string) (model.Action, error) {
|
||||
switch actionStr {
|
||||
case string(model.ActionCreate):
|
||||
return model.ActionCreate, nil
|
||||
case string(model.ActionRead):
|
||||
return model.ActionRead, nil
|
||||
case string(model.ActionUpdate):
|
||||
return model.ActionUpdate, nil
|
||||
case string(model.ActionDelete):
|
||||
return model.ActionDelete, nil
|
||||
default:
|
||||
return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr))
|
||||
}
|
||||
}
|
||||
126
api/pkg/auth/internal/casbin/config/config.go
Normal file
126
api/pkg/auth/internal/casbin/config/config.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
mongodbadapter "github.com/casbin/mongodb-adapter/v3"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AdapterConfig struct {
|
||||
DatabaseName *string `mapstructure:"database_name"`
|
||||
DatabaseNameEnv *string `mapstructure:"database_name_env"`
|
||||
CollectionName *string `mapstructure:"collection_name"`
|
||||
CollectionNameEnv *string `mapstructure:"collection_name_env"`
|
||||
TimeoutSeconds *int `mapstructure:"timeout_seconds"`
|
||||
TimeoutSecondsEnv *string `mapstructure:"timeout_seconds_env"`
|
||||
IsFiltered *bool `mapstructure:"is_filtered"`
|
||||
IsFilteredEnv *string `mapstructure:"is_filtered_env"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ModelPath *string `mapstructure:"model_path"`
|
||||
ModelPathEnv *string `mapstructure:"model_path_env"`
|
||||
Adapter *AdapterConfig `mapstructure:"adapter"`
|
||||
}
|
||||
|
||||
type EnforcerConfig struct {
|
||||
ModelPath string
|
||||
Adapter *mongodbadapter.AdapterConfig
|
||||
}
|
||||
|
||||
func getEnvValue(logger mlogger.Logger, varName, envVarName string, value, envValue *string) string {
|
||||
if value != nil && envValue != nil {
|
||||
logger.Warn("Both variable and environment variable are set, using environment variable value",
|
||||
zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.String("value", *value), zap.String("env_value", os.Getenv(*envValue)))
|
||||
}
|
||||
|
||||
if envValue != nil {
|
||||
return os.Getenv(*envValue)
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
return *value
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func getEnvIntValue(logger mlogger.Logger, varName, envVarName string, value *int, envValue *string) int {
|
||||
if value != nil && envValue != nil {
|
||||
logger.Warn("Both variable and environment variable are set, using environment variable value",
|
||||
zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.Int("value", *value), zap.String("env_value", os.Getenv(*envValue)))
|
||||
}
|
||||
|
||||
if envValue != nil {
|
||||
envStr := os.Getenv(*envValue)
|
||||
if envStr != "" {
|
||||
if parsed, err := time.ParseDuration(envStr + "s"); err == nil {
|
||||
return int(parsed.Seconds())
|
||||
}
|
||||
logger.Warn("Invalid environment variable value for timeout", zap.String("environment_variable", envVarName), zap.String("value", envStr))
|
||||
}
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
return *value
|
||||
}
|
||||
|
||||
return 30 // Default timeout in seconds
|
||||
}
|
||||
|
||||
func getEnvBoolValue(logger mlogger.Logger, varName, envVarName string, value *bool, envValue *string) bool {
|
||||
if value != nil && envValue != nil {
|
||||
logger.Warn("Both variable and environment variable are set, using environment variable value",
|
||||
zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.Bool("value", *value), zap.String("env_value", os.Getenv(*envValue)))
|
||||
}
|
||||
|
||||
if envValue != nil {
|
||||
envStr := os.Getenv(*envValue)
|
||||
if envStr == "true" || envStr == "1" {
|
||||
return true
|
||||
} else if envStr == "false" || envStr == "0" {
|
||||
return false
|
||||
}
|
||||
logger.Warn("Invalid environment variable value for boolean", zap.String("environment_variable", envVarName), zap.String("value", envStr))
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
return *value
|
||||
}
|
||||
|
||||
return false // Default for boolean
|
||||
}
|
||||
|
||||
func PrepareConfig(logger mlogger.Logger, config *Config) (*EnforcerConfig, error) {
|
||||
if config == nil {
|
||||
return nil, merrors.Internal("No configuration provided")
|
||||
}
|
||||
|
||||
adapter := &mongodbadapter.AdapterConfig{
|
||||
DatabaseName: getEnvValue(logger, "database_name", "database_name_env", config.Adapter.DatabaseName, config.Adapter.DatabaseNameEnv),
|
||||
CollectionName: getEnvValue(logger, "collection_name", "collection_name_env", config.Adapter.CollectionName, config.Adapter.CollectionNameEnv),
|
||||
Timeout: time.Duration(getEnvIntValue(logger, "timeout_seconds", "timeout_seconds_env", config.Adapter.TimeoutSeconds, config.Adapter.TimeoutSecondsEnv)) * time.Second,
|
||||
IsFiltered: getEnvBoolValue(logger, "is_filtered", "is_filtered_env", config.Adapter.IsFiltered, config.Adapter.IsFilteredEnv),
|
||||
}
|
||||
|
||||
if len(adapter.DatabaseName) == 0 {
|
||||
logger.Error("Database name is not set")
|
||||
return nil, merrors.InvalidArgument("database name must be provided")
|
||||
}
|
||||
|
||||
path := getEnvValue(logger, "model_path", "model_path_env", config.ModelPath, config.ModelPathEnv)
|
||||
|
||||
logger.Info("Configuration prepared",
|
||||
zap.String("model_path", path),
|
||||
zap.String("database_name", adapter.DatabaseName),
|
||||
zap.String("collection_name", adapter.CollectionName),
|
||||
zap.Duration("timeout", adapter.Timeout),
|
||||
zap.Bool("is_filtered", adapter.IsFiltered),
|
||||
)
|
||||
|
||||
return &EnforcerConfig{ModelPath: path, Adapter: adapter}, nil
|
||||
}
|
||||
206
api/pkg/auth/internal/casbin/enforcer.go
Normal file
206
api/pkg/auth/internal/casbin/enforcer.go
Normal file
@@ -0,0 +1,206 @@
|
||||
// casbin_enforcer.go
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/tech/sendico/pkg/auth/anyobject"
|
||||
cc "github.com/tech/sendico/pkg/auth/internal/casbin/config"
|
||||
"github.com/tech/sendico/pkg/auth/internal/casbin/serialization"
|
||||
"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"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CasbinEnforcer implements the Enforcer interface using Casbin.
|
||||
type CasbinEnforcer struct {
|
||||
logger mlogger.Logger
|
||||
enforcer *casbin.Enforcer
|
||||
roleSerializer serialization.Role
|
||||
permissionSerializer serialization.Policy
|
||||
}
|
||||
|
||||
// NewCasbinEnforcer initializes a new CasbinEnforcer with a MongoDB adapter, logger, and PolicySerializer.
|
||||
// The 'domain' parameter is no longer stored internally, as the interface requires passing a domainRef per method call.
|
||||
func NewEnforcer(
|
||||
logger mlogger.Logger,
|
||||
client *mongo.Client,
|
||||
settings model.SettingsT,
|
||||
) (*CasbinEnforcer, error) {
|
||||
var config cc.Config
|
||||
if err := mapstructure.Decode(settings, &config); err != nil {
|
||||
logger.Warn("Failed to decode Casbin configuration", zap.Error(err), zap.Any("settings", settings))
|
||||
return nil, merrors.Internal("failed to decode Casbin configuration")
|
||||
}
|
||||
|
||||
// Create a Casbin adapter + enforcer from your config and client.
|
||||
l := logger.Named("enforcer")
|
||||
e, err := createAdapter(l, &config, client)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create Casbin enforcer", zap.Error(err))
|
||||
return nil, merrors.Internal("failed to create Casbin enforcer")
|
||||
}
|
||||
|
||||
logger.Info("Casbin enforcer created")
|
||||
return &CasbinEnforcer{
|
||||
logger: l,
|
||||
enforcer: e,
|
||||
permissionSerializer: serialization.NewPolicySerializer(),
|
||||
roleSerializer: serialization.NewRoleSerializer(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Enforce checks if a user has the specified action permission on an object within a domain.
|
||||
func (c *CasbinEnforcer) Enforce(
|
||||
_ context.Context,
|
||||
permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID,
|
||||
action model.Action,
|
||||
) (bool, error) {
|
||||
// Convert ObjectIDs to strings for Casbin
|
||||
account := accountRef.Hex()
|
||||
organization := organizationRef.Hex()
|
||||
permission := permissionRef.Hex()
|
||||
object := anyobject.ID
|
||||
if objectRef != primitive.NilObjectID {
|
||||
object = objectRef.Hex()
|
||||
}
|
||||
act := string(action)
|
||||
|
||||
c.logger.Debug("Enforcing policy",
|
||||
zap.String("account", account), zap.String("organization", organization),
|
||||
zap.String("permission", permission), zap.String("object", object),
|
||||
zap.String("action", act))
|
||||
|
||||
// Perform the enforcement
|
||||
result, err := c.enforcer.Enforce(account, organization, permission, object, act)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to enforce policy", zap.Error(err),
|
||||
zap.String("account", account), zap.String("organization", organization),
|
||||
zap.String("permission", permission), zap.String("object", object),
|
||||
zap.String("action", act))
|
||||
return false, err
|
||||
}
|
||||
|
||||
c.logger.Debug("Policy enforcement result", zap.Bool("result", result))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// EnforceBatch checks a user’s permission for multiple objects at once.
|
||||
// It returns a map from objectRef -> boolean indicating whether access is granted.
|
||||
func (c *CasbinEnforcer) 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))
|
||||
for _, desc := range objectRefs {
|
||||
ok, err := c.Enforce(ctx, desc.GetPermissionRef(), accountRef, desc.GetOrganizationRef(), *desc.GetID(), action)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to enforce", zap.Error(err), mzap.ObjRef("permission_ref", desc.GetPermissionRef()),
|
||||
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()),
|
||||
mzap.ObjRef("object_ref", *desc.GetID()), zap.String("action", string(action)))
|
||||
return nil, err
|
||||
}
|
||||
results[*desc.GetID()] = ok
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetRoles retrieves all roles assigned to the user within the domain.
|
||||
func (c *CasbinEnforcer) GetRoles(ctx context.Context, accountRef, orgRef primitive.ObjectID) ([]model.Role, error) {
|
||||
sub := accountRef.Hex()
|
||||
dom := orgRef.Hex()
|
||||
|
||||
c.logger.Debug("Fetching roles for user", zap.String("subject", sub), zap.String("domain", dom))
|
||||
|
||||
// Get all roles for the user in the domain
|
||||
sroles, err := c.enforcer.GetFilteredGroupingPolicy(0, sub, "", dom)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to get roles from policies", zap.Error(err),
|
||||
zap.String("account_ref", sub), zap.String("organization_ref", dom),
|
||||
)
|
||||
return nil, merrors.Internal("failed to fetch roles from policies")
|
||||
}
|
||||
|
||||
roles := make([]model.Role, 0, len(sroles))
|
||||
for _, srole := range sroles {
|
||||
role, err := c.roleSerializer.Deserialize(srole)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to deserialize role", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
roles = append(roles, *role)
|
||||
}
|
||||
|
||||
c.logger.Debug("Roles fetched successfully", zap.Int("count", len(roles)))
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
// GetPermissions retrieves all effective policies for the user within the domain.
|
||||
func (c *CasbinEnforcer) GetPermissions(ctx context.Context, accountRef, orgRef primitive.ObjectID) ([]model.Role, []model.Permission, error) {
|
||||
c.logger.Debug("Fetching policies for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef))
|
||||
|
||||
// Step 1: Retrieve all roles assigned to the user within the domain
|
||||
roles, err := c.GetRoles(ctx, accountRef, orgRef)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to get roles", zap.Error(err))
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Map to hold unique policies
|
||||
permissionsMap := make(map[string]*model.Permission)
|
||||
for _, role := range roles {
|
||||
// Step 2a: Retrieve all policies associated with the role within the domain
|
||||
policies, err := c.enforcer.GetFilteredPolicy(0, role.DescriptionRef.Hex())
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to get policies for role", zap.Error(err), mzap.ObjRef("role_ref", role.DescriptionRef))
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2b: Process each policy to extract Permission, Action, and Effect
|
||||
for _, policy := range policies {
|
||||
|
||||
if len(policy) < 5 {
|
||||
c.logger.Warn("Incomplete policy encountered", zap.Strings("policy", policy))
|
||||
continue // Ensure the policy line has enough fields
|
||||
}
|
||||
|
||||
// Deserialize the policy using
|
||||
deserializedPolicy, err := c.permissionSerializer.Deserialize(policy)
|
||||
if err != nil {
|
||||
c.logger.Warn("Failed to deserialize policy", zap.Error(err), zap.Strings("policy", policy))
|
||||
continue
|
||||
}
|
||||
|
||||
// Construct a unique key combining Permission ID and Action to prevent duplicates
|
||||
policyKey := deserializedPolicy.DescriptionRef.Hex() + ":" + string(deserializedPolicy.Effect.Action)
|
||||
if _, exists := permissionsMap[policyKey]; exists {
|
||||
continue // Policy-action pair already accounted for
|
||||
}
|
||||
|
||||
// Add the Policy to the map
|
||||
permissionsMap[policyKey] = &model.Permission{
|
||||
RolePolicy: *deserializedPolicy,
|
||||
AccountRef: accountRef,
|
||||
}
|
||||
c.logger.Debug("Policy added to policyMap", zap.Any("policy_key", policyKey))
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the map to a slice
|
||||
permissions := make([]model.Permission, 0, len(permissionsMap))
|
||||
for _, permission := range permissionsMap {
|
||||
permissions = append(permissions, *permission)
|
||||
}
|
||||
|
||||
c.logger.Debug("Permissions fetched successfully", zap.Int("count", len(permissions)))
|
||||
return roles, permissions, nil
|
||||
}
|
||||
34
api/pkg/auth/internal/casbin/factory.go
Normal file
34
api/pkg/auth/internal/casbin/factory.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"github.com/casbin/casbin/v2"
|
||||
mongodbadapter "github.com/casbin/mongodb-adapter/v3"
|
||||
cc "github.com/tech/sendico/pkg/auth/internal/casbin/config"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func createAdapter(logger mlogger.Logger, config *cc.Config, client *mongo.Client) (*casbin.Enforcer, error) {
|
||||
dbc, err := cc.PrepareConfig(logger, config)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to prepare database configuration", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adapter, err := mongodbadapter.NewAdapterByDB(client, dbc.Adapter)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create DB adapter", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e, err := casbin.NewEnforcer(dbc.ModelPath, adapter, NewCasbinLogger(logger))
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create permissions enforcer", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
e.EnableAutoSave(true)
|
||||
|
||||
// No need to manually load policy. Casbin does it for us
|
||||
return e, nil
|
||||
}
|
||||
61
api/pkg/auth/internal/casbin/logger.go
Normal file
61
api/pkg/auth/internal/casbin/logger.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CasbinZapLogger wraps a zap.Logger to implement Casbin's Logger interface.
|
||||
type CasbinZapLogger struct {
|
||||
logger mlogger.Logger
|
||||
}
|
||||
|
||||
// NewCasbinLogger constructs a new CasbinZapLogger.
|
||||
func NewCasbinLogger(logger mlogger.Logger) *CasbinZapLogger {
|
||||
return &CasbinZapLogger{
|
||||
logger: logger.Named("driver"),
|
||||
}
|
||||
}
|
||||
|
||||
// EnableLog enables or disables logging.
|
||||
func (l *CasbinZapLogger) EnableLog(_ bool) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// IsEnabled returns whether logging is currently enabled.
|
||||
func (l *CasbinZapLogger) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LogModel is called by Casbin when loading model settings (you can customize if you want).
|
||||
func (l *CasbinZapLogger) LogModel(m [][]string) {
|
||||
l.logger.Info("Model loaded", zap.Any("model", m))
|
||||
}
|
||||
|
||||
func (l *CasbinZapLogger) LogPolicy(m map[string][][]string) {
|
||||
l.logger.Info("Policy loaded", zap.Int("entries", len(m)))
|
||||
}
|
||||
|
||||
func (l *CasbinZapLogger) LogError(err error, msg ...string) {
|
||||
// If no custom message was passed, log a generic one
|
||||
if len(msg) == 0 {
|
||||
l.logger.Warn("Error occurred", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, join any provided messages and include them
|
||||
l.logger.Warn(strings.Join(msg, " "), zap.Error(err))
|
||||
}
|
||||
|
||||
// LogEnforce is called by Casbin to log each Enforce() call if logging is enabled.
|
||||
func (l *CasbinZapLogger) LogEnforce(matcher string, request []any, result bool, explains [][]string) {
|
||||
l.logger.Debug("Enforcing policy...", zap.String("matcher", matcher), zap.Any("request", request),
|
||||
zap.Bool("result", result), zap.Any("explains", explains))
|
||||
}
|
||||
|
||||
// LogRole is called by Casbin when role manager adds or deletes a role.
|
||||
func (l *CasbinZapLogger) LogRole(roles []string) {
|
||||
l.logger.Debug("Changing roles...", zap.Strings("roles", roles))
|
||||
}
|
||||
54
api/pkg/auth/internal/casbin/manager.go
Normal file
54
api/pkg/auth/internal/casbin/manager.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// package casbin
|
||||
|
||||
package casbin
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// CasbinManager implements the auth.Manager interface by aggregating Role and Permission managers.
|
||||
type CasbinManager 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 *CasbinEnforcer,
|
||||
settings model.SettingsT,
|
||||
) (*CasbinManager, 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 &CasbinManager{
|
||||
logger: logger,
|
||||
roleManager: NewRoleManager(logger, enforcer, pdesc.ID, rdb),
|
||||
permManager: NewPermissionManager(logger, enforcer),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Permission returns the Permission manager.
|
||||
func (m *CasbinManager) Permission() management.Permission {
|
||||
return m.permManager
|
||||
}
|
||||
|
||||
// Role returns the Role manager.
|
||||
func (m *CasbinManager) Role() management.Role {
|
||||
return m.roleManager
|
||||
}
|
||||
54
api/pkg/auth/internal/casbin/models/auth.conf
Normal file
54
api/pkg/auth/internal/casbin/models/auth.conf
Normal file
@@ -0,0 +1,54 @@
|
||||
######################################################
|
||||
# Request Definition
|
||||
######################################################
|
||||
[request_definition]
|
||||
# Explanation:
|
||||
# - `accountRef`: The account (user) making the request.
|
||||
# - `organizationRef`: The organization in which the role applies.
|
||||
# - `permissionRef`: The specific permission being requested.
|
||||
# - `objectRef`: The object/resource being accessed (specific object or all objects).
|
||||
# - `action`: The action being requested (CRUD: read, write, update, delete).
|
||||
r = accountRef, organizationRef, permissionRef, objectRef, action
|
||||
|
||||
|
||||
######################################################
|
||||
# Policy Definition
|
||||
######################################################
|
||||
[policy_definition]
|
||||
# Explanation:
|
||||
# - `roleRef`: The role to which the policy is assigned.
|
||||
# - `organizationRef`: The organization in which the role applies.
|
||||
# - `permissionRef`: The permission associated with the policy.
|
||||
# - `objectRef`: The specific object/resource the policy applies to (or all objects).
|
||||
# - `action`: The CRUD action permitted or denied.
|
||||
# - `eft`: Effect of the policy (`allow` or `deny`).
|
||||
p = roleRef, organizationRef, permissionRef, objectRef, action, eft
|
||||
|
||||
|
||||
######################################################
|
||||
# Role Definition
|
||||
######################################################
|
||||
[role_definition]
|
||||
# Explanation:
|
||||
# - Maps `accountRef` (user) to `roleRef` (role) within `organizationRef` (scope).
|
||||
# Casbin requires underscores for placeholders, so we do not literally use accountRef, roleRef, etc. here.
|
||||
g = _, _, _
|
||||
|
||||
|
||||
######################################################
|
||||
# Policy Effect
|
||||
######################################################
|
||||
[policy_effect]
|
||||
# Explanation:
|
||||
# - Grants access if any `allow` policy matches and no `deny` policies match.
|
||||
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
|
||||
|
||||
|
||||
######################################################
|
||||
# Matchers
|
||||
######################################################
|
||||
[matchers]
|
||||
# Explanation:
|
||||
# - Checks if the user (accountRef) belongs to the roleRef within an organizationRef via `g()`.
|
||||
# - Ensures the organizationRef, permissionRef, objectRef, and action match the policy.
|
||||
m = g(r.accountRef, p.roleRef, r.organizationRef) && r.organizationRef == p.organizationRef && r.permissionRef == p.permissionRef && (p.objectRef == r.objectRef || p.objectRef == "*") && r.action == p.action
|
||||
167
api/pkg/auth/internal/casbin/permissions.go
Normal file
167
api/pkg/auth/internal/casbin/permissions.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/auth/anyobject"
|
||||
"github.com/tech/sendico/pkg/auth/internal/casbin/serialization"
|
||||
"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"
|
||||
)
|
||||
|
||||
// CasbinPermissionManager manages permissions using Casbin.
|
||||
type CasbinPermissionManager struct {
|
||||
logger mlogger.Logger // Logger for logging operations
|
||||
enforcer *CasbinEnforcer // Casbin enforcer for managing policies
|
||||
serializer serialization.Policy // Serializer for converting policies to/from Casbin
|
||||
}
|
||||
|
||||
// GrantToRole adds a permission to a role in Casbin.
|
||||
func (m *CasbinPermissionManager) GrantToRole(ctx context.Context, policy *model.RolePolicy) error {
|
||||
objRef := anyobject.ID
|
||||
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)),
|
||||
)
|
||||
|
||||
// Serialize permission
|
||||
serializedPolicy, err := m.serializer.Serialize(policy)
|
||||
if err != nil {
|
||||
m.logger.Error("Failed to serialize permission while granting permission", zap.Error(err),
|
||||
mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
|
||||
mzap.ObjRef("permission_ref", policy.DescriptionRef),
|
||||
mzap.ObjRef("organization_ref", policy.OrganizationRef),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Add policy to Casbin
|
||||
added, err := m.enforcer.enforcer.AddPolicy(serializedPolicy...)
|
||||
if err != nil {
|
||||
m.logger.Error("Failed to add policy to Casbin", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if added {
|
||||
m.logger.Info("Policy added to Casbin",
|
||||
mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
|
||||
mzap.ObjRef("permission_ref", policy.DescriptionRef),
|
||||
zap.String("object_ref", objRef),
|
||||
)
|
||||
} else {
|
||||
m.logger.Warn("Policy already exists in Casbin",
|
||||
mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
|
||||
mzap.ObjRef("permission_ref", policy.DescriptionRef),
|
||||
zap.String("object_ref", objRef),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeFromRole removes a permission from a role in Casbin.
|
||||
func (m *CasbinPermissionManager) RevokeFromRole(ctx context.Context, policy *model.RolePolicy) error {
|
||||
objRef := anyobject.ID
|
||||
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)),
|
||||
)
|
||||
|
||||
// Serialize policy
|
||||
serializedPolicy, err := m.serializer.Serialize(policy)
|
||||
if err != nil {
|
||||
m.logger.Error("Failed to serialize policy while revoking permission from role",
|
||||
zap.Error(err), mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
|
||||
mzap.ObjRef("policy_ref", policy.DescriptionRef))
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove policy from Casbin
|
||||
removed, err := m.enforcer.enforcer.RemovePolicy(serializedPolicy...)
|
||||
if err != nil {
|
||||
m.logger.Error("Failed to remove policy from Casbin", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if removed {
|
||||
m.logger.Info("Policy removed from Casbin",
|
||||
mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
|
||||
mzap.ObjRef("permission_ref", policy.DescriptionRef),
|
||||
zap.String("object_ref", objRef),
|
||||
)
|
||||
} else {
|
||||
m.logger.Warn("Policy does not exist in Casbin",
|
||||
mzap.ObjRef("role_ref", policy.RoleDescriptionRef),
|
||||
mzap.ObjRef("permission_ref", policy.DescriptionRef),
|
||||
zap.String("object_ref", objRef),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPolicies retrieves all policies for a specific role.
|
||||
func (m *CasbinPermissionManager) GetPolicies(
|
||||
ctx context.Context,
|
||||
roleRef primitive.ObjectID,
|
||||
) ([]model.RolePolicy, error) {
|
||||
m.logger.Debug("Fetching policies for role", mzap.ObjRef("role_ref", roleRef))
|
||||
|
||||
// Retrieve Casbin policies for the role
|
||||
policies, err := m.enforcer.enforcer.GetFilteredPolicy(0, roleRef.Hex())
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to get policies", zap.Error(err), mzap.ObjRef("role_ref", roleRef))
|
||||
return nil, err
|
||||
}
|
||||
if len(policies) == 0 {
|
||||
m.logger.Info("No policies found for role", mzap.ObjRef("role_ref", roleRef))
|
||||
return nil, merrors.NoData("no policies")
|
||||
}
|
||||
|
||||
// Deserialize policies
|
||||
var result []model.RolePolicy
|
||||
for _, policy := range policies {
|
||||
permission, err := m.serializer.Deserialize(policy)
|
||||
if err != nil {
|
||||
m.logger.Warn("Failed to deserialize policy", zap.Error(err), zap.String("policy", policy[0]))
|
||||
continue
|
||||
}
|
||||
result = append(result, *permission)
|
||||
}
|
||||
|
||||
m.logger.Debug("Policies fetched successfully", mzap.ObjRef("role_ref", roleRef), zap.Int("count", len(result)))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Save persists changes to the Casbin policy store.
|
||||
func (m *CasbinPermissionManager) Save() error {
|
||||
if err := m.enforcer.enforcer.SavePolicy(); err != nil {
|
||||
m.logger.Error("Failed to save policies in Casbin", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
m.logger.Info("Policies successfully saved in Casbin")
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPermissionManager(logger mlogger.Logger, enforcer *CasbinEnforcer) *CasbinPermissionManager {
|
||||
return &CasbinPermissionManager{
|
||||
logger: logger.Named("permission"),
|
||||
enforcer: enforcer,
|
||||
serializer: serialization.NewPolicySerializer(),
|
||||
}
|
||||
}
|
||||
209
api/pkg/auth/internal/casbin/role.go
Normal file
209
api/pkg/auth/internal/casbin/role.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package casbin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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 *CasbinEnforcer
|
||||
rdb role.DB
|
||||
rolePermissionRef primitive.ObjectID
|
||||
}
|
||||
|
||||
// NewRoleManager creates a new RoleManager.
|
||||
func NewRoleManager(logger mlogger.Logger, enforcer *CasbinEnforcer, 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
|
||||
}
|
||||
|
||||
// removePolicies removes policies based on the provided filter and logs the results.
|
||||
func (rm *RoleManager) removePolicies(policyType, role string, roleRef primitive.ObjectID) error {
|
||||
filterIndex := 1
|
||||
if policyType == "permission" {
|
||||
filterIndex = 0
|
||||
}
|
||||
policies, err := rm.enforcer.enforcer.GetFilteredPolicy(filterIndex, role)
|
||||
if err != nil {
|
||||
rm.logger.Warn("Failed to fetch "+policyType+" policies", zap.Error(err), mzap.ObjRef("role_ref", roleRef))
|
||||
return err
|
||||
}
|
||||
|
||||
for _, policy := range policies {
|
||||
args := make([]any, len(policy))
|
||||
for i, v := range policy {
|
||||
args[i] = v
|
||||
}
|
||||
var removed bool
|
||||
var removeErr error
|
||||
if policyType == "grouping" {
|
||||
removed, removeErr = rm.enforcer.enforcer.RemoveGroupingPolicy(args...)
|
||||
} else {
|
||||
removed, removeErr = rm.enforcer.enforcer.RemovePolicy(args...)
|
||||
}
|
||||
|
||||
if removeErr != nil {
|
||||
rm.logger.Warn("Failed to remove "+policyType+" policy for role", zap.Error(removeErr), mzap.ObjRef("role_ref", roleRef), zap.Strings("policy", policy))
|
||||
return removeErr
|
||||
}
|
||||
if removed {
|
||||
rm.logger.Info("Removed "+policyType+" policy for role", mzap.ObjRef("role_ref", roleRef), zap.Strings("policy", policy))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchRolesFromPolicies retrieves and converts policies to roles.
|
||||
func (rm *RoleManager) fetchRolesFromPolicies(policies [][]string, orgRef primitive.ObjectID) []model.RoleDescription {
|
||||
roles := make([]model.RoleDescription, 0, len(policies))
|
||||
for _, policy := range policies {
|
||||
if len(policy) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
roleID, err := primitive.ObjectIDFromHex(policy[1])
|
||||
if err != nil {
|
||||
rm.logger.Warn("Invalid role ID", zap.String("roleID", policy[1]))
|
||||
continue
|
||||
}
|
||||
roles = append(roles, model.RoleDescription{Base: storable.Base{ID: roleID}, OrganizationRef: orgRef})
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
// Create creates a new role in an organization.
|
||||
func (rm *RoleManager) Create(ctx context.Context, orgRef primitive.ObjectID, description *model.Describable) (*model.RoleDescription, error) {
|
||||
if err := rm.validateObjectIDs(orgRef); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
role := &model.RoleDescription{
|
||||
Describable: *description,
|
||||
OrganizationRef: orgRef,
|
||||
}
|
||||
if err := rm.rdb.Create(ctx, role); err != nil {
|
||||
rm.logger.Warn("Failed to create role", zap.Error(err), mzap.ObjRef("organiztion_ref", orgRef))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rm.logger.Info("Role created successfully", mzap.StorableRef(role), mzap.ObjRef("organization_ref", orgRef))
|
||||
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
|
||||
}
|
||||
|
||||
sub := role.AccountRef.Hex()
|
||||
roleID := role.DescriptionRef.Hex()
|
||||
domain := role.OrganizationRef.Hex()
|
||||
|
||||
added, err := rm.enforcer.enforcer.AddGroupingPolicy(sub, roleID, domain)
|
||||
return rm.logPolicyResult("assign", added, 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
|
||||
}
|
||||
|
||||
role := roleRef.Hex()
|
||||
|
||||
// Remove grouping policies
|
||||
if err := rm.removePolicies("grouping", role, roleRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove permission policies
|
||||
if err := rm.removePolicies("permission", role, roleRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// // Save changes
|
||||
// if err := rm.enforcer.enforcer.SavePolicy(); err != nil {
|
||||
// rm.logger.Warn("Failed to save Casbin policies after role deletion",
|
||||
// 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, orgRef primitive.ObjectID) error {
|
||||
if err := rm.validateObjectIDs(roleRef, accountRef, orgRef); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sub := accountRef.Hex()
|
||||
role := roleRef.Hex()
|
||||
domain := orgRef.Hex()
|
||||
|
||||
removed, err := rm.enforcer.enforcer.RemoveGroupingPolicy(sub, role, domain)
|
||||
return rm.logPolicyResult("revoke", removed, err, roleRef, accountRef, orgRef)
|
||||
}
|
||||
|
||||
// logPolicyResult logs results for Assign and Revoke.
|
||||
func (rm *RoleManager) logPolicyResult(action string, result bool, err error, roleRef, accountRef, orgRef 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", orgRef))
|
||||
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", orgRef))
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves all roles in an organization or all roles if orgRef is zero.
|
||||
func (rm *RoleManager) List(ctx context.Context, orgRef primitive.ObjectID) ([]model.RoleDescription, error) {
|
||||
domain := orgRef.Hex()
|
||||
groupingPolicies, err := rm.enforcer.enforcer.GetFilteredGroupingPolicy(2, domain)
|
||||
if err != nil {
|
||||
rm.logger.Warn("Failed to fetch grouping policies", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := rm.fetchRolesFromPolicies(groupingPolicies, orgRef)
|
||||
|
||||
rm.logger.Info("Retrieved roles for organization", mzap.ObjRef("organization_ref", orgRef), zap.Int("count", len(roles)))
|
||||
return roles, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package serializationimp
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/auth/anyobject"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// PolicySerializer implements CasbinSerializer for Permission.
|
||||
type PolicySerializer struct{}
|
||||
|
||||
// Serialize converts a Permission object into a Casbin policy.
|
||||
func (s *PolicySerializer) Serialize(entity *model.RolePolicy) ([]any, error) {
|
||||
if entity.RoleDescriptionRef.IsZero() ||
|
||||
entity.OrganizationRef.IsZero() ||
|
||||
entity.DescriptionRef.IsZero() || // Ensure permissionRef is valid
|
||||
entity.Effect.Action == "" || // Ensure action is not empty
|
||||
entity.Effect.Effect == "" { // Ensure effect (eft) is not empty
|
||||
return nil, merrors.InvalidArgument("permission contains invalid object references or missing fields")
|
||||
}
|
||||
|
||||
objectRef := anyobject.ID
|
||||
if entity.ObjectRef != nil {
|
||||
objectRef = entity.ObjectRef.Hex()
|
||||
}
|
||||
|
||||
return []any{
|
||||
entity.RoleDescriptionRef.Hex(), // Maps to p.roleRef
|
||||
entity.OrganizationRef.Hex(), // Maps to p.organizationRef
|
||||
entity.DescriptionRef.Hex(), // Maps to p.permissionRef
|
||||
objectRef, // Maps to p.objectRef (wildcard if empty)
|
||||
string(entity.Effect.Action), // Maps to p.action
|
||||
string(entity.Effect.Effect), // Maps to p.eft
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Deserialize converts a Casbin policy into a Permission object.
|
||||
func (s *PolicySerializer) Deserialize(policy []string) (*model.RolePolicy, error) {
|
||||
if len(policy) != 6 { // Ensure policy has the correct number of fields
|
||||
return nil, merrors.Internal("invalid policy format")
|
||||
}
|
||||
|
||||
roleRef, err := primitive.ObjectIDFromHex(policy[0])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid roleRef in policy")
|
||||
}
|
||||
|
||||
organizationRef, err := primitive.ObjectIDFromHex(policy[1])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid organizationRef in policy")
|
||||
}
|
||||
|
||||
permissionRef, err := primitive.ObjectIDFromHex(policy[2])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid permissionRef in policy")
|
||||
}
|
||||
|
||||
// Handle wildcard for ObjectRef
|
||||
var objectRef *primitive.ObjectID
|
||||
if policy[3] != anyobject.ID {
|
||||
ref, err := primitive.ObjectIDFromHex(policy[3])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid objectRef in policy")
|
||||
}
|
||||
objectRef = &ref
|
||||
}
|
||||
|
||||
return &model.RolePolicy{
|
||||
RoleDescriptionRef: roleRef,
|
||||
Policy: model.Policy{
|
||||
OrganizationRef: organizationRef,
|
||||
DescriptionRef: permissionRef,
|
||||
ObjectRef: objectRef,
|
||||
Effect: model.ActionEffect{
|
||||
Action: model.Action(policy[4]),
|
||||
Effect: model.Effect(policy[5]),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
57
api/pkg/auth/internal/casbin/serialization/internal/role.go
Normal file
57
api/pkg/auth/internal/casbin/serialization/internal/role.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package serializationimp
|
||||
|
||||
import (
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// RoleSerializer implements CasbinSerializer for Role.
|
||||
type RoleSerializer struct{}
|
||||
|
||||
// Serialize converts a Role object into a Casbin grouping policy.
|
||||
func (s *RoleSerializer) Serialize(entity *model.Role) ([]any, error) {
|
||||
// Validate required fields
|
||||
if entity.AccountRef.IsZero() || entity.DescriptionRef.IsZero() || entity.OrganizationRef.IsZero() {
|
||||
return nil, merrors.InvalidArgument("role contains invalid object references")
|
||||
}
|
||||
|
||||
return []any{
|
||||
entity.AccountRef.Hex(), // Maps to g(_, _, _) accountRef
|
||||
entity.DescriptionRef.Hex(), // Maps to g(_, _, _) roleRef
|
||||
entity.OrganizationRef.Hex(), // Maps to g(_, _, _) organizationRef
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Deserialize converts a Casbin grouping policy into a Role object.
|
||||
func (s *RoleSerializer) Deserialize(policy []string) (*model.Role, error) {
|
||||
// Ensure the policy has exactly 3 fields
|
||||
if len(policy) != 3 {
|
||||
return nil, merrors.Internal("invalid grouping policy format")
|
||||
}
|
||||
|
||||
// Parse accountRef
|
||||
accountRef, err := primitive.ObjectIDFromHex(policy[0])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid accountRef in grouping policy")
|
||||
}
|
||||
|
||||
// Parse roleDescriptionRef (roleRef)
|
||||
roleDescriptionRef, err := primitive.ObjectIDFromHex(policy[1])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid roleRef in grouping policy")
|
||||
}
|
||||
|
||||
// Parse organizationRef
|
||||
organizationRef, err := primitive.ObjectIDFromHex(policy[2])
|
||||
if err != nil {
|
||||
return nil, merrors.InvalidArgument("invalid organizationRef in grouping policy")
|
||||
}
|
||||
|
||||
// Return the constructed Role object
|
||||
return &model.Role{
|
||||
AccountRef: accountRef,
|
||||
DescriptionRef: roleDescriptionRef,
|
||||
OrganizationRef: organizationRef,
|
||||
}, nil
|
||||
}
|
||||
12
api/pkg/auth/internal/casbin/serialization/policy.go
Normal file
12
api/pkg/auth/internal/casbin/serialization/policy.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
serializationimp "github.com/tech/sendico/pkg/auth/internal/casbin/serialization/internal"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type Policy = CasbinSerializer[model.RolePolicy]
|
||||
|
||||
func NewPolicySerializer() Policy {
|
||||
return &serializationimp.PolicySerializer{}
|
||||
}
|
||||
12
api/pkg/auth/internal/casbin/serialization/role.go
Normal file
12
api/pkg/auth/internal/casbin/serialization/role.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package serialization
|
||||
|
||||
import (
|
||||
serializationimp "github.com/tech/sendico/pkg/auth/internal/casbin/serialization/internal"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
)
|
||||
|
||||
type Role = CasbinSerializer[model.Role]
|
||||
|
||||
func NewRoleSerializer() Role {
|
||||
return &serializationimp.RoleSerializer{}
|
||||
}
|
||||
10
api/pkg/auth/internal/casbin/serialization/serializer.go
Normal file
10
api/pkg/auth/internal/casbin/serialization/serializer.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package serialization
|
||||
|
||||
// CasbinSerializer defines methods for serializing and deserializing any Casbin-compatible entity.
|
||||
type CasbinSerializer[T any] interface {
|
||||
// Serialize converts an entity (Role or Permission) into a Casbin policy.
|
||||
Serialize(entity *T) ([]any, error)
|
||||
|
||||
// Deserialize converts a Casbin policy into an entity (Role or Permission).
|
||||
Deserialize(policy []string) (*T, error)
|
||||
}
|
||||
151
api/pkg/auth/internal/native/db/policies.go
Normal file
151
api/pkg/auth/internal/native/db/policies.go
Normal 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
|
||||
}
|
||||
99
api/pkg/auth/internal/native/db/roles.go
Normal file
99
api/pkg/auth/internal/native/db/roles.go
Normal 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
|
||||
}
|
||||
27
api/pkg/auth/internal/native/dbpolicies.go
Normal file
27
api/pkg/auth/internal/native/dbpolicies.go
Normal 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)
|
||||
}
|
||||
24
api/pkg/auth/internal/native/dbroles.go
Normal file
24
api/pkg/auth/internal/native/dbroles.go
Normal 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
747
api/pkg/auth/internal/native/enforcer_test.go
Normal file
747
api/pkg/auth/internal/native/enforcer_test.go
Normal 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
|
||||
51
api/pkg/auth/internal/native/manager.go
Normal file
51
api/pkg/auth/internal/native/manager.go
Normal 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
|
||||
}
|
||||
BIN
api/pkg/auth/internal/native/native.test
Executable file
BIN
api/pkg/auth/internal/native/native.test
Executable file
Binary file not shown.
17
api/pkg/auth/internal/native/nstructures/policies.go
Normal file
17
api/pkg/auth/internal/native/nstructures/policies.go
Normal 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"
|
||||
}
|
||||
15
api/pkg/auth/internal/native/nstructures/role.go
Normal file
15
api/pkg/auth/internal/native/nstructures/role.go
Normal 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"
|
||||
}
|
||||
101
api/pkg/auth/internal/native/permission.go
Normal file
101
api/pkg/auth/internal/native/permission.go
Normal 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,
|
||||
}
|
||||
}
|
||||
142
api/pkg/auth/internal/native/role.go
Normal file
142
api/pkg/auth/internal/native/role.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user