Decorator System¶
Overview¶
Guard-core's decorator system allows users to apply per-route security settings using Python decorators. The system is built on three layers:
RouteConfig-- a data class holding all per-route security settings.BaseSecurityDecorator-- manages route config storage, route ID generation, and event dispatching.- Mixin classes -- provide decorator methods grouped by concern (access control, rate limiting, authentication, etc.).
SecurityDecorator-- the final class combining all mixins, ready for use.
RouteConfig¶
Defined in guard_core/decorators/base.py, RouteConfig holds every per-route override:
class RouteConfig:
def __init__(self) -> None:
self.rate_limit: int | None = None
self.rate_limit_window: int | None = None
self.ip_whitelist: list[str] | None = None
self.ip_blacklist: list[str] | None = None
self.blocked_countries: list[str] | None = None
self.whitelist_countries: list[str] | None = None
self.bypassed_checks: set[str] = set()
self.require_https: bool = False
self.auth_required: str | None = None
self.custom_validators: list[Callable] = []
self.blocked_user_agents: list[str] = []
self.required_headers: dict[str, str] = {}
self.behavior_rules: list[BehaviorRule] = []
self.block_cloud_providers: set[str] = set()
self.max_request_size: int | None = None
self.allowed_content_types: list[str] | None = None
self.time_restrictions: dict[str, str] | None = None
self.enable_suspicious_detection: bool = True
self.require_referrer: list[str] | None = None
self.api_key_required: bool = False
self.session_limits: dict[str, int] | None = None
self.geo_rate_limits: dict[str, tuple[int, int]] | None = None
When a decorator is applied to a route function, it creates or updates a RouteConfig and associates it with that function's route ID.
BaseSecurityDecorator¶
The base class manages the decorator lifecycle:
class BaseSecurityDecorator:
def __init__(self, config: SecurityConfig) -> None:
self.config = config
self._route_configs: dict[str, RouteConfig] = {}
self.behavior_tracker = BehaviorTracker(config)
self.agent_handler: Any = None
def get_route_config(self, route_id: str) -> RouteConfig | None:
return self._route_configs.get(route_id)
def _get_route_id(self, func: Callable[..., Any]) -> str:
return f"{func.__module__}.{func.__qualname__}"
def _ensure_route_config(self, func: Callable[..., Any]) -> RouteConfig:
route_id = self._get_route_id(func)
if route_id not in self._route_configs:
config = RouteConfig()
config.enable_suspicious_detection = (
self.config.enable_penetration_detection
)
self._route_configs[route_id] = config
return self._route_configs[route_id]
def _apply_route_config(self, func: Callable[..., Any]) -> Callable[..., Any]:
route_id = self._get_route_id(func)
func._guard_route_id = route_id
return func
Key points:
- Route ID is
"{module}.{qualname}"of the decorated function. This ensures uniqueness across the application. _guard_route_idis stamped onto the function object. The routing system uses this attribute to look up theRouteConfigat request time._ensure_route_configcreates aRouteConfigon first access and reuses it for stacked decorators on the same function.
Mixin Classes¶
Guard-core provides six mixin classes, each adding a category of decorators:
| Mixin | Decorators Provided |
|---|---|
AccessControlMixin |
require_ip(), block_countries(), allow_countries(), block_clouds(), bypass() |
RateLimitingMixin |
rate_limit() |
AuthenticationMixin |
require_auth(), require_api_key(), require_https() |
ContentFilteringMixin |
max_body_size(), allowed_content_types(), require_headers(), block_user_agents(), require_referrer() |
BehavioralMixin |
track_behavior() |
AdvancedMixin |
time_window(), custom_validator(), disable_detection() |
Each mixin follows the same pattern. For example, AccessControlMixin.require_ip():
class AccessControlMixin(BaseSecurityMixin):
def require_ip(
self,
whitelist: list[str] | None = None,
blacklist: list[str] | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
route_config = self._ensure_route_config(func)
if whitelist:
route_config.ip_whitelist = whitelist
if blacklist:
route_config.ip_blacklist = blacklist
return self._apply_route_config(func)
return decorator
SecurityDecorator¶
The final class combines everything via multiple inheritance:
class SecurityDecorator(
BaseSecurityDecorator,
AccessControlMixin,
RateLimitingMixin,
BehavioralMixin,
AuthenticationMixin,
ContentFilteringMixin,
AdvancedMixin,
):
pass
How Adapters Expose Decorators¶
Your adapter creates a SecurityDecorator instance and makes it available to users. The typical pattern:
1. Create the Instance¶
from guard_core.decorators import SecurityDecorator
from guard_core.models import SecurityConfig
config = SecurityConfig(...)
guard = SecurityDecorator(config)
2. Register It with the Middleware¶
The middleware needs access to the decorator handler to resolve route configs. Store it on the app's state and pass it to the middleware:
The RouteConfigResolver looks for the decorator in two places:
def get_guard_decorator(self, app: Any) -> BaseSecurityDecorator | None:
if app and hasattr(app, "state") and hasattr(app.state, "guard_decorator"):
app_guard_decorator = app.state.guard_decorator
if isinstance(app_guard_decorator, BaseSecurityDecorator):
return app_guard_decorator
return self.context.guard_decorator if self.context.guard_decorator else None
3. Users Apply Decorators¶
@app.get("/admin")
@guard.require_ip(whitelist=["10.0.0.0/8"])
@guard.rate_limit(max_requests=5, window=60)
@guard.require_https()
async def admin_panel():
return {"status": "ok"}
Multiple decorators stack. Each one calls _ensure_route_config() which returns the same RouteConfig instance for the function, so all settings accumulate.
Route Resolution at Request Time¶
When a request arrives, the pipeline needs to find the RouteConfig for the matched route. This happens in two places:
RouteConfigResolver (Middleware Level)¶
Used by BypassHandler and the dispatch method. It iterates the app's route table:
def get_route_config(self, request: GuardRequest) -> RouteConfig | None:
app = request.scope.get("app")
guard_decorator = self.get_guard_decorator(app)
if not guard_decorator:
return None
if not app:
return None
path = request.url_path
method = request.method
for route in app.routes:
is_match, route_id = self.is_matching_route(route, path, method)
if is_match and route_id:
return guard_decorator.get_route_config(route_id)
return None
A route matches when:
route.path == request.url_pathrequest.method in route.methodsroute.endpointhas a_guard_route_idattribute (stamped by the decorator)
get_route_decorator_config (Check Level)¶
Used inside individual security checks via guard_core/decorators/base.py:
def get_route_decorator_config(
request: GuardRequest, decorator_handler: BaseSecurityDecorator
) -> RouteConfig | None:
if hasattr(request, "scope") and "route" in request.scope:
route = request.scope["route"]
if hasattr(route, "endpoint") and hasattr(route.endpoint, "_guard_route_id"):
route_id = route.endpoint._guard_route_id
return decorator_handler.get_route_config(route_id)
return None
This function looks at request.scope["route"] directly, which is populated by the framework's routing system.
Adapter Responsibility¶
Your adapter must ensure that:
request.scope["app"]returns the application instance with a.routesattribute (list of route objects).- Each route object has
.path,.methods, and.endpointattributes. request.scope["route"]is set to the matched route object (for check-level resolution).
For ASGI frameworks (Starlette, FastAPI), this is automatic. For WSGI frameworks (Flask, Django), you must populate the scope dict in your GuardRequest wrapper.
The send_decorator_event Mechanism¶
When a security check blocks a request due to a decorator setting, it can emit an event through the decorator's event system. BaseSecurityDecorator provides several event methods:
async def send_decorator_event(
self,
event_type: str,
request: GuardRequest,
action_taken: str,
reason: str,
decorator_type: str,
**kwargs: Any,
) -> None:
if not self.agent_handler:
return
# ... builds SecurityEvent and sends to agent_handler
Convenience methods built on top:
send_access_denied_event()-- blocked by access control rulessend_authentication_failed_event()-- auth check failuresend_rate_limit_event()-- rate limit exceededsend_decorator_violation_event()-- generic decorator violation
These events flow to the Guard Agent platform when enable_agent is True. The event bus at the middleware level (SecurityEventBus) handles middleware-level events, while BaseSecurityDecorator handles decorator-level events. Both ultimately send to the same agent handler.
Extending the Decorator System¶
If your framework needs additional decorator methods, create a new mixin and combine it:
from guard_core.decorators import SecurityDecorator
from guard_core.decorators.base import BaseSecurityMixin
class MyFrameworkMixin(BaseSecurityMixin):
def require_session(self, session_key: str):
def decorator(func):
route_config = self._ensure_route_config(func)
route_config.session_limits = {session_key: 1}
return self._apply_route_config(func)
return decorator
class MyFrameworkDecorator(SecurityDecorator, MyFrameworkMixin):
pass
Then use MyFrameworkDecorator instead of SecurityDecorator in your adapter.
Initializing the Decorator¶
The decorator needs async initialization for behavior tracking and agent integration. Call these during your middleware's initialize():
async def initialize(self) -> None:
if self.guard_decorator:
if self.redis_handler:
await self.guard_decorator.initialize_behavior_tracking(
self.redis_handler
)
if self.agent_handler:
await self.guard_decorator.initialize_agent(self.agent_handler)
HandlerInitializer.initialize_agent_integrations() handles the agent part automatically if you pass the decorator to it: