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

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

View File

@@ -0,0 +1,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))
}
}

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

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

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

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

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

View 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

View 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(),
}
}

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

View File

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

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

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

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

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