Adding Custom Authorization Adapters
This guide provides comprehensive instructions for developers looking to integrate a new authorization engine with Castlecraft Architect. By creating a plugin, you can enable all architect authorization
sub-commands for your chosen engine.
The system is designed with a clear separation of concerns:
- Generation: Synchronous, build-time tasks like creating model and policy files.
- Persistence: Asynchronous, runtime tasks like syncing policies to a database.
A full-featured plugin will provide implementations for both.
1. Understanding the AuthorizationEngineAdapter
Interface
The AuthorizationEngineAdapter
is an Abstract Base Class (ABC) located at castlecraft_architect.infrastructure.authorization.adapters.auth_engine_adapter.py
. It defines a contract for all engine-specific interactions. Implementing this interface allows the Architect tool to:
- Generate Model Definitions: Create the structural schema or model configuration required by your engine (e.g., Casbin's
model.conf
). This is primarily used by thearchitect generate authorization-model
CLI command. - Generate Bootstrap Policies: Create initial policy rules based on permissions discovered within the application and a user-provided engine-specific configuration.
- Announce a Persistence Service: Point to a service that can handle runtime operations.
Key methods to implement are:
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Type
from castlecraft_architect.infrastructure.authorization.policy_persistence_service import (
PolicyPersistenceService,
)
# PolicyRule is a flexible dictionary representing a single policy rule.
PolicyRule = Dict[str, Any]
class AuthorizationEngineAdapter(ABC):
@abstractmethod
def get_engine_name(self) -> str:
"""Returns the unique, lowercase string name of the engine (e.g., 'casbin', 'spicedb')."""
@abstractmethod
def generate_model_definition(self, **kwargs: Any) -> str:
"""
Generates the structural model definition string for the engine.
Receives generic CLI options via **kwargs (e.g., include_scope: bool).
"""
@abstractmethod
def generate_bootstrap_policy_data(
self,
declared_permissions: List[Dict[str, Any]],
bootstrap_config: Dict[str, Any],
**kwargs: Any,
) -> str:
"""
Generates initial policy data as a string based on application permissions
and a generic bootstrap configuration dictionary.
Receives generic CLI options via **kwargs (e.g., include_scope: bool, default_domain: str).
"""
def get_bootstrap_policy_file_extension(self) -> str:
"""Returns the typical file extension for this engine's bootstrap policy data (e.g., 'csv', 'json')."""
return "txt"
def get_persistence_service_class(
self,
) -> Optional[Type[PolicyPersistenceService]]:
"""
Returns the corresponding PolicyPersistenceService class for this engine, if available.
"""
return None
2. Creating Your Authorization Adapter Plugin
To add support for a new authorization engine, you'll create a Python package that acts as a plugin for Architect.
2.1. Plugin Project Structure
A typical structure for your plugin (e.g., architect-myengine-adapter
) would be:
architect-myengine-adapter/
├── pyproject.toml
└── src/
└── architect_myengine_adapter/ # Your plugin's Python package
├── __init__.py
├── adapter.py # Contains your MyEngineAdapter implementation
└── plugin.py # Contains the plugin registration function
2.2. Implementing the Adapter (e.g., MyEngineAdapter
)
In your adapter.py
(or similar file within your plugin's package, e.g., src/architect_myengine_adapter/adapter.py
), define a class that inherits from AuthorizationEngineAdapter
.
__init__
and get_engine_name
# In src/architect_myengine_adapter/adapter.py
import logging
from typing import List, Dict, Any, Optional # Ensure necessary imports
# Import the base class from Architect.
# Your plugin will need Architect installed as a dependency.
from castlecraft_architect.infrastructure.authorization.adapters.auth_engine_adapter import AuthorizationEngineAdapter, PolicyRule
logger = logging.getLogger(__name__)
class MyEngineAdapter(AuthorizationEngineAdapter):
def __init__(self, engine_specific_config_for_generation: Optional[Dict[str, Any]] = None):
# This constructor is called when your adapter is instantiated by the plugin registration function.
# It typically doesn't need live clients or enforcers for generation tasks.
# The 'engine_specific_config_for_generation' argument is optional and generally not used
# when adapters are instantiated via the plugin system, as configuration is passed
# to the generation methods directly.
logger.info(f"MyEngineAdapter for generation initialized.")
def get_engine_name(self) -> str:
return "myengine" # Must be unique and lowercase
generate_model_definition
Implement this to return a string representing your engine's model structure. The CLI command architect generate authorization-model --engine myengine
will invoke this.
Generic CLI options like --include-scope
are passed via **kwargs
.
Reference: CasbinEngineAdapter.generate_model_definition
generates the content for a model.conf
file.
# In src/architect_myengine_adapter/adapter.py (continued)
def generate_model_definition(self, **kwargs: Any) -> str:
# CLI options like 'include_scope' are passed in kwargs
include_scope = kwargs.get("include_scope", False)
# Example: if MyEngine uses a JSON schema for its model
model_structure = {
"version": "1.0",
"schema": {
"user": {"type": "string"},
"resource": {"type": "string"},
"action": {"type": "string"}
}
}
if include_scope:
model_structure["schema"]["scope"] = {"type": "string"}
import json # Make sure json is imported if not already
return json.dumps(model_structure, indent=2)
generate_bootstrap_policy_data
This method generates the initial set of policy rules for your engine. It's called by architect generate authorization-policy --engine myengine
.
Input Parameters:
declared_permissions: List[Dict[str, Any]]
: A list of all permissions discovered by Architect. Each dictionary contains:permission_name: str
(Unique identifier for the permission)resource: str
(e.g., "Order", "Document")actions: List[str]
(e.g.,["create", "read"]
)scope: Optional[str]
(e.g., "europe", "own")description: Optional[str]
bootstrap_config: Dict[str, Any]]
: A dictionary parsed from the YAML/JSON file provided via the--engine-config
CLI option. Your adapter must define how it interprets this configuration. It's recommended to look for a top-level key matching your engine's name (e.g.,bootstrap_config.get("myengine", {})
).**kwargs: Any
: Contains generic CLI options likeinclude_scope: bool
anddefault_domain: str
that your adapter can choose to use if relevant.
Handling --engine-config
:
Your adapter should parse bootstrap_config
to find its specific settings. For example:
# In MyEngineAdapter.generate_bootstrap_policy_data
myengine_settings = bootstrap_config.get(self.get_engine_name(), {})
initial_user_attributes = myengine_settings.get("user_attributes", {})
default_rules = myengine_settings.get("default_rules", [])
# ... use these settings to generate policies ...
An example engine-config.yaml
for MyEngineAdapter
might be:
myengine:
user_attributes:
alice: { department: "finance", region: "emea" }
default_rules:
- { subject_department: "finance", resource_type: "report/finance/*", action: "view" }
Handling --grant-full-access-to <PRINCIPAL_IDENTIFIER>
:
If the --grant-full-access-to <PRINCIPAL_IDENTIFIER>
CLI flag is used, the bootstrap_config
dictionary will contain a special key: "_grant_full_access_details"
. This will be a dictionary with the following structure:
{
"principal": "<PRINCIPAL_IDENTIFIER_FROM_CLI>",
"all_permission_names": ["permission_name_1", "permission_name_2", ...]
}
Your adapter should check for this _grant_full_access_details
key. If present, use details["principal"]
as the subject/role to grant access to, and details["all_permission_names"]
as the list of permissions. The interpretation of how to grant 'full access' is engine-specific (RBAC, ReBAC, ABAC, etc.).
# In MyEngineAdapter.generate_bootstrap_policy_data (example handling)
full_access_details = bootstrap_config.get("_grant_full_access_details")
if full_access_details:
principal_to_grant = full_access_details.get("principal")
permissions_to_grant = full_access_details.get("all_permission_names", [])
# Logic to grant all 'permissions_to_grant' to 'principal_to_grant'
# This logic will vary based on whether MyEngine is RBAC, ReBAC, ABAC, etc.
pass
Reference: CasbinEngineAdapter.generate_bootstrap_policy_data
for an RBAC-centric example.
get_bootstrap_policy_file_extension
Return the typical file extension for your engine's policy data (e.g., "csv", "json", "rego").
# In src/architect_myengine_adapter/adapter.py (continued)
def get_bootstrap_policy_file_extension(self) -> str:
return "json" # Example for MyEngine if it uses JSON policies
2.3. Plugin Registration Function
In your plugin.py
(e.g., src/architect_myengine_adapter/plugin.py
), you'll define a function that Architect will call to register your adapter. This function receives an AuthorizationAdapterRegistrationAPI
instance.
# In src/architect_myengine_adapter/plugin.py
import logging
from typing import TYPE_CHECKING
import punq # For type hinting the container
from .adapter import MyEngineAdapter # Import your adapter implementation
if TYPE_CHECKING:
# Import the API from Architect.
# Your plugin will need Architect installed as a dependency.
from castlecraft_architect.plugins.authorization_adapter_api import AuthorizationAdapterRegistrationAPI
logger = logging.getLogger(__name__)
def register_my_engine_adapter(
api: "AuthorizationAdapterRegistrationAPI",
container: "punq.Container", # The DI container
plugin_name: str
):
"""
This function is the entry point for Architect to load this authorization adapter plugin.
The 'plugin_name' argument is the name of the entry point as defined in pyproject.toml.
The 'container' argument provides access to the application's DI container.
"""
logger.info(f"Plugin '{plugin_name}': Registering MyEngineAdapter.")
# Example: Using the container to resolve a service (e.g., Settings)
# from castlecraft_architect.config import Settings
# settings = container.resolve(Settings)
my_adapter_instance = MyEngineAdapter()
api.register_adapter(my_adapter_instance)
logger.info(f"Plugin '{plugin_name}': MyEngineAdapter registered successfully.")
2.4. pyproject.toml
Configuration
Your plugin needs a pyproject.toml
file at the root of its project directory (e.g., architect-myengine-adapter/pyproject.toml
) to define itself as a package and declare the entry point for Architect.
# In architect-myengine-adapter/pyproject.toml
[project]
name = "architect-myengine-adapter"
version = "0.1.0"
description = "An authorization adapter for MyEngine for Castlecraft Architect."
# ... other project metadata (authors, license, dependencies for your adapter, etc.) ...
requires-python = ">=3.11"
dependencies = [
"castlecraft-architect" # Your plugin depends on the core Architect package
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project.entry-points."architect.authorization_adapters"]
# 'myengine_plugin_entrypoint' is the unique name for this plugin's registration hook.
# 'architect_myengine_adapter.plugin:register_my_engine_adapter' is the importable path
# to your registration function (module_name.file_name:function_name).
myengine_plugin_entrypoint = "architect_myengine_adapter.plugin:register_my_engine_adapter"
2.5. Installing Your Plugin
For Architect to discover your plugin, it must be installed in the same Python environment where Architect is running. During development, you can install it in editable mode from the root of your plugin's project directory:
pip install -e .
Or, if using uv
:
uv pip install -e .
3. Dynamic Policy Management (Runtime CRUD Operations)
Dynamic CRUD (Create, Read, Update, Delete) operations on policies are not part of the AuthorizationEngineAdapter
interface. This interface is focused on the generation of model definitions and bootstrap policy data, which are typically offline or build-time tasks performed by the CLI.
For runtime policy management, you should create a separate service, similar to CasbinAuthorizationService
(castlecraft_architect.infrastructure.authorization.casbin_authorization_service.py
). This service would:
- Be initialized with a live, configured client or enforcer for your specific authorization engine (e.g., an initialized Casbin
AsyncEnforcer
). - Expose methods like
add_policy_rule(rule: PolicyRule)
,remove_policy_rule(rule: PolicyRule)
,get_all_policy_rules()
, etc. - Be registered with your application's Dependency Injection (DI) container for use by API endpoints or other application services.
- Handling Model Variations: Your runtime service should be robust to variations in the model definition if your engine supports them. For instance, the
CasbinAuthorizationService
inspects the loaded Casbin model to determine if domains/scopes are enabled and adjusts itsenforce
calls accordingly (e.g., using 3-part or 4-part requests). This makes the service more flexible if the generated model (e.g., fromgenerate_model_definition
) can vary based on options like--include-scope
. This separation keeps the concerns of static generation (via adapters) distinct from dynamic runtime management (via services).
4. Considerations for Different Authorization Models
ReBAC (Relationship-Based Access Control - e.g., SpiceDB, Authzed)
generate_model_definition
: Would generate the schema definition (e.g., SpiceDB Schema Definition Language). This defines resource types, relations, and permissions derived from relations.generate_bootstrap_policy_data
: Would generate relationship tuples. Thebootstrap_config
(from--engine-config
) would define initial object instances and their relationships (e.g.,user:alice
ismember
ofgroup:editors
). The adapter translatesdeclared_permissions
into these relationships.--grant-full-access-to <PRINCIPAL>
: The adapter would use the provided<PRINCIPAL>
(e.g.,user:super_admin
) to create broad relationships, like making this principal an 'owner' or equivalent on all relevant resource types derived fromall_permission_names
or its schema.
ABAC (Attribute-Based Access Control - e.g., OPA/Rego)
generate_model_definition
: Might generate Rego utility functions or define the expected structure of input data for policies. Could be a no-op if the model is implicit.generate_bootstrap_policy_data
: Would generate Rego policy files (.rego
).bootstrap_config
could define initial subject/resource attributes or templates for policy rules.declared_permissions
helps understand the universe of actions/resources.--grant-full-access-to <PRINCIPAL>
: Typically translates to a high-priority Rego rule. If<PRINCIPAL>
is intended as a role name, it might beinput.subject.attributes.role == "<PRINCIPAL>"
. If it's a direct subject ID,input.subject.id == "<PRINCIPAL>"
.
5. Testing Your Adapter
Thoroughly test your adapter by:
- Using the
architect generate auth-model --engine myengine ...
command to verify model definition output. - Ensuring your plugin is installed in the Architect environment.
- Creating sample permission components in your Architect project state.
- Creating a sample
engine-config.yaml
for your engine. - Using
architect generate authorization-policy --engine myengine --engine-config path/to/your/config.yaml ...
to verify bootstrap policy output. - Testing the
--grant-full-access-to <your_principal_id>
flag. - Separately, implementing and testing your runtime authorization service (e.g.,
MyEngineAuthorizationService
) for policy enforcement and dynamic CRUD operations.
Conclusion
By creating an authorization adapter plugin, you provide a standardized way for Castlecraft Architect to interact with your chosen authorization engine. This modular approach enhances the tool's flexibility and allows the community to contribute support for a wide range of authorization solutions.