diff --git a/docs/security/authorization.md b/docs/security/authorization.md index d30962bd..c80f888a 100644 --- a/docs/security/authorization.md +++ b/docs/security/authorization.md @@ -1 +1,42 @@ -# Coming Soon +# Authorization + +Authorization is a crucial security feature in Ellar that determines what resources authenticated users can access. Ellar provides a flexible and powerful authorization system through policies, roles, and claims. + +## Table of Contents + +1. [Basic Authorization](#basic-authorization) +2. [Policies](./authorization/policies.md) +3. [Role-Based Authorization](./authorization/role-based.md) +4. [Claims-Based Authorization](./authorization/claims-based.md) +5. [Custom Policies with Requirements](./authorization/custom-policies.md) +6. [Combining Policies](./authorization/combining-policies.md) + +## Basic Authorization + +To use authorization in your Ellar application, you need to: + +1. Decorate your controllers or routes with `@Authorize()` +2. Apply specific policies using `@CheckPolicies()` +3. Ensure users are authenticated using `@AuthenticationRequired()` + +Here's a basic example: + +```python +from ellar.auth import AuthenticationRequired, Authorize, CheckPolicies +from ellar.common import Controller, get + +@Controller("/articles") +@Authorize() # Enable authorization for all routes +@AuthenticationRequired() # Require authentication +class ArticleController: + @get("/admin") + @CheckPolicies(RolePolicy("admin")) # Only allow admins + async def admin_dashboard(self): + return "Admin Dashboard" + + @get("/public") + async def public_articles(self): # Accessible to any authenticated user + return "Public Articles" +``` + +For detailed information about specific authorization features, please refer to the respective sections in the documentation. diff --git a/docs/security/authorization/claims-based.md b/docs/security/authorization/claims-based.md new file mode 100644 index 00000000..bb5799fe --- /dev/null +++ b/docs/security/authorization/claims-based.md @@ -0,0 +1,141 @@ +# Claims-Based Authorization + +Claims-based authorization provides a more flexible and granular approach to authorization compared to role-based authorization. Claims are key-value pairs that represent attributes of the user and their access rights. + +## Using ClaimsPolicy + +Ellar provides the `ClaimsPolicy` class for implementing claims-based authorization: + +```python +from ellar.auth import AuthenticationRequired, Authorize, CheckPolicies +from ellar.auth.policy import ClaimsPolicy +from ellar.common import Controller, get + +@Controller("/articles") +@Authorize() +class ArticleController: + @get("/create") + @CheckPolicies(ClaimsPolicy("article", "create")) + async def create_article(self): + return "Create Article" + + @get("/publish") + @CheckPolicies(ClaimsPolicy("article", "create", "publish")) + async def publish_article(self): + return "Publish Article" +``` + +## How ClaimsPolicy Works + +The `ClaimsPolicy` checks if the user has specific claim values for a given claim type. Claims are typically stored in the user's identity: + +```python +# Example user data structure with claims +user_data = { + "id": "123", + "username": "john_doe", + "article": ["create", "read", "publish"], # Claim type: "article" with multiple values + "subscription": "premium" # Claim type: "subscription" with single value +} +``` + +## Single vs Multiple Claim Values + +Claims can have single or multiple values: + +```python +@Controller("/content") +@Authorize() +class ContentController: + @get("/premium") + @CheckPolicies(ClaimsPolicy("subscription", "premium")) # Single claim value + async def premium_content(self): + return "Premium Content" + + @get("/manage") + @CheckPolicies(ClaimsPolicy("permissions", "create", "edit", "delete")) # Multiple claim values + async def manage_content(self): + return "Content Management" +``` + +## Combining Claims Policies + +You can combine multiple claims policies using logical operators: + +```python +@Controller("/advanced") +@Authorize() +class AdvancedController: + @get("/editor") + @CheckPolicies( + ClaimsPolicy("article", "edit") & + ClaimsPolicy("status", "active") + ) + async def editor_dashboard(self): + return "Editor Dashboard" + + @get("/moderator") + @CheckPolicies( + ClaimsPolicy("content", "moderate") | + ClaimsPolicy("role", "admin") + ) + async def moderator_dashboard(self): + return "Moderator Dashboard" +``` + +## Best Practices + +1. Use descriptive claim types and values +2. Keep claim values simple and atomic +3. Use claims for fine-grained permissions +4. Consider using claims instead of roles for more flexible authorization +5. Document your claim types and their possible values + +## Example: E-commerce Authorization + +Here's a comprehensive example showing claims-based authorization in an e-commerce application: + +```python +@Controller("/store") +@Authorize() +class StoreController: + @get("/products") + @CheckPolicies(ClaimsPolicy("store", "view_products")) + async def view_products(self): + return "Product List" + + @get("/products/manage") + @CheckPolicies( + ClaimsPolicy("store", "manage_products") & + ClaimsPolicy("account_status", "verified") + ) + async def manage_products(self): + return "Product Management" + + @get("/orders") + @CheckPolicies( + ClaimsPolicy("store", "view_orders") | + ClaimsPolicy("role", "customer_service") + ) + async def view_orders(self): + return "Order List" + + @get("/reports") + @CheckPolicies( + ClaimsPolicy("store", "view_reports") & + (ClaimsPolicy("role", "manager") | ClaimsPolicy("permissions", "analytics")) + ) + async def view_reports(self): + return "Store Reports" +``` + +## Claims vs Roles + +While roles are a form of claims, dedicated claims offer several advantages: + +1. **Granularity**: Claims can represent specific permissions rather than broad role categories +2. **Flexibility**: Claims can be easily added or modified without changing role structures +3. **Clarity**: Claims directly express what a user can do rather than implying it through roles +4. **Scalability**: Claims can grow with your application's needs without role explosion + +For complex authorization scenarios, consider combining claims with roles and custom policies. See [Combining Policies](./combining-policies.md) for more information. diff --git a/docs/security/authorization/combining-policies.md b/docs/security/authorization/combining-policies.md new file mode 100644 index 00000000..d216188a --- /dev/null +++ b/docs/security/authorization/combining-policies.md @@ -0,0 +1,204 @@ +# Combining Policies + +Ellar provides powerful operators to combine different types of policies for complex authorization scenarios. This guide shows you how to use these combinations effectively. + +## Basic Policy Operators + +Ellar supports three logical operators for combining policies: + +- `&` (AND): Both policies must return `True` +- `|` (OR): At least one policy must return `True` +- `~` (NOT): Inverts the policy result + +## Simple Combinations + +Here are basic examples of combining policies: + +```python +from ellar.auth import AuthenticationRequired, Authorize, CheckPolicies +from ellar.auth.policy import RolePolicy, ClaimsPolicy +from ellar.common import Controller, get + +@Controller("/examples") +@Authorize() +@AuthenticationRequired() +class ExampleController: + @get("/and-example") + @CheckPolicies(RolePolicy("admin") & RolePolicy("editor")) + async def requires_both_roles(self): + return "Must be both admin and editor" + + @get("/or-example") + @CheckPolicies(RolePolicy("admin") | RolePolicy("moderator")) + async def requires_either_role(self): + return "Must be either admin or moderator" + + @get("/not-example") + @CheckPolicies(~RolePolicy("banned")) + async def not_banned(self): + return "Access allowed if not banned" +``` + +## Complex Combinations + +You can create more complex authorization rules by combining multiple policies: + +```python +@Controller("/advanced") +@Authorize() +@AuthenticationRequired() +class AdvancedController: + @get("/complex") + @CheckPolicies( + (RolePolicy("editor") & ClaimsPolicy("department", "content")) | + RolePolicy("admin") + ) + async def complex_access(self): + return "Complex access rules" + + @get("/nested") + @CheckPolicies( + RolePolicy("user") & + (ClaimsPolicy("subscription", "premium") | RolePolicy("staff")) & + ~RolePolicy("restricted") + ) + async def nested_rules(self): + return "Nested policy rules" +``` + +## Combining Different Policy Types + +You can mix and match different types of policies: + +```python +from ellar.auth import RolePolicy, ClaimsPolicy +from ellar.common import Controller, get +from .custom_policies import AgeRequirementPolicy, TeamMemberPolicy + +@Controller("/mixed") +@Authorize() +class MixedPolicyController: + @get("/content") + @CheckPolicies( + (AgeRequirementPolicy[18] & ClaimsPolicy("region", "US", "CA")) | + RolePolicy("global_admin") + ) + async def age_and_region(self): + return "Age and region restricted content" + + @get("/team-access") + @CheckPolicies( + TeamMemberPolicy["engineering"] & + (RolePolicy("developer") | RolePolicy("team_lead")) & + ClaimsPolicy("security_clearance", "level2") + ) + async def team_access(self): + return "Team-specific access" +``` + +## Real-World Examples + +Here are some practical examples of policy combinations: + +### Content Management System + +```python +@Controller("/cms") +@Authorize() +class CMSController: + @get("/articles/{id}/edit") + @CheckPolicies( + (RolePolicy("editor") & ClaimsPolicy("article", "edit")) | + RolePolicy("admin") | + (TeamMemberPolicy["content"] & ClaimsPolicy("article", "edit")) + ) + async def edit_article(self): + return "Edit Article" + + @get("/articles/{id}/publish") + @CheckPolicies( + (RolePolicy("editor") & ClaimsPolicy("article", "publish") & ~RolePolicy("junior")) | + RolePolicy("senior_editor") | + RolePolicy("admin") + ) + async def publish_article(self): + return "Publish Article" +``` + +### E-commerce Platform + +```python +@Controller("/store") +@Authorize() +class StoreController: + @get("/products/{id}/manage") + @CheckPolicies( + (RolePolicy("vendor") & ClaimsPolicy("store", "manage_products")) | + RolePolicy("store_admin") + ) + async def manage_product(self): + return "Manage Product" + + @get("/orders/{id}/refund") + @CheckPolicies( + (RolePolicy("support") & ClaimsPolicy("order", "refund") & AgeRequirementPolicy[21]) | + RolePolicy("finance_admin") + ) + async def process_refund(self): + return "Process Refund" +``` + +## Best Practices + +1. **Readability** + - Use parentheses to make complex combinations clear + - Break long policy combinations into multiple lines + - Consider creating custom policies for very complex rules + +2. **Performance** + - Order OR conditions with the most likely to succeed first + - Order AND conditions with the least expensive to evaluate first + - Consider caching policy results for expensive evaluations + +3. **Maintenance** + - Document complex policy combinations + - Create reusable policy combinations for common patterns + - Keep policy logic modular and testable + +4. **Security** + - Always start with the principle of least privilege + - Use OR combinations carefully as they broaden access + - Regularly audit policy combinations for security implications + +## Common Patterns + +Here are some common patterns for combining policies: + +```python +# Role hierarchy +base_access = RolePolicy("user") +elevated_access = base_access & RolePolicy("premium") +admin_access = elevated_access & RolePolicy("admin") + +# Feature access with fallback +feature_access = ( + ClaimsPolicy("feature", "beta") & RolePolicy("beta_tester") +) | RolePolicy("admin") + +# Geographic restrictions with age verification +regional_access = ( + AgeRequirementPolicy[21] & + ClaimsPolicy("region", "US", "CA") +) | RolePolicy("global_access") + +# Team-based access with role requirements +team_access = ( + TeamMemberPolicy["project-x"] & + (RolePolicy("developer") | RolePolicy("designer")) +) & ~RolePolicy("restricted") +``` + +For more specific examples of each policy type, refer to: +- [Role-Based Authorization](./role-based.md) +- [Claims-Based Authorization](./claims-based.md) +- [Custom Policies with Requirements](./custom-policies.md) diff --git a/docs/security/authorization/custom-policies.md b/docs/security/authorization/custom-policies.md new file mode 100644 index 00000000..e6981702 --- /dev/null +++ b/docs/security/authorization/custom-policies.md @@ -0,0 +1,197 @@ +# Custom Policies with Requirements + +Custom policies with requirements provide the most flexible way to implement authorization logic in Ellar. They allow you to pass additional parameters to your policies and implement complex authorization rules. + +## Creating a Policy with Requirements + +To create a policy with requirements, inherit from `PolicyWithRequirement`: + +```python +from ellar.auth import PolicyWithRequirement +from ellar.common import IExecutionContext +from ellar.di import injectable +import typing as t + +@injectable +class AgeRequirementPolicy(PolicyWithRequirement): + async def handle(self, context: IExecutionContext, requirement: t.Any) -> bool: + min_age = requirement.values()[0] # Access the first requirement argument + user_age = int(context.user.get("age", 0)) + return user_age >= min_age + +@injectable +class TeamMemberPolicy(PolicyWithRequirement): + async def handle(self, context: IExecutionContext, requirement: t.Any) -> bool: + team_name = requirement.values()[0] + user_teams = context.user.get("teams", []) + return team_name in user_teams +``` + +## Using Policies with Requirements + +Apply policies with requirements using square bracket notation: + +```python +from ellar.auth import AuthenticationRequired, Authorize, CheckPolicies +from ellar.common import Controller, get + +@Controller("/content") +@Authorize() +class ContentController: + @get("/adult") + @CheckPolicies(AgeRequirementPolicy[21]) # Requires age >= 21 + async def adult_content(self): + return "Adult Content" + + @get("/team") + @CheckPolicies(TeamMemberPolicy["engineering"]) # Requires membership in engineering team + async def team_content(self): + return "Team Content" +``` + +## Multiple Requirements + +You can pass multiple requirements to a policy: + +```python +@injectable +class ProjectAccessPolicy(PolicyWithRequirement): + async def handle(self, context: IExecutionContext, requirement: t.Any) -> bool: + project_id = requirement.arg_1 + access_level = requirement.arg_2 + + user_projects = context.user.get("projects", {}) + user_access = user_projects.get(project_id) + + return user_access and user_access >= access_level + +@Controller("/projects") +@Authorize() +@AuthenticationRequired() +class ProjectController: + @get("/{project_id}/edit") + @CheckPolicies(ProjectAccessPolicy["project-123", "write"]) + async def edit_project(self): + return "Edit Project" + + @get("/{project_id}/admin") + @CheckPolicies(ProjectAccessPolicy["project-123", "admin"]) + async def admin_project(self): + return "Project Administration" +``` + +## Custom Requirement Types + +You can define custom requirement types for more structured requirements: + +```python +from ellar.common.compatible import AttributeDict + +class ProjectRequirement(AttributeDict): + def __init__(self, project_id: str, min_access_level: str) -> None: + super().__init__({ + "project_id": project_id, + "min_access_level": min_access_level + }) + +@injectable +class EnhancedProjectPolicy(PolicyWithRequirement): + requirement_type = ProjectRequirement # Specify custom requirement type + + async def handle(self, context: IExecutionContext, requirement: ProjectRequirement) -> bool: + user_projects = context.user.get("projects", {}) + user_access = user_projects.get(requirement.project_id) + + return user_access and user_access >= requirement.min_access_level + +@Controller("/enhanced-projects") +@Authorize() +@AuthenticationRequired() +class EnhancedProjectController: + @get("/{project_id}/manage") + @CheckPolicies(EnhancedProjectPolicy[ProjectRequirement("project-123", "manage")]) + async def manage_project(self): + return "Manage Project" +``` + +## Complex Example: Multi-Factor Authorization + +Here's an example of a policy that requires both age verification and location-based access: + +```python +class RegionRequirement(AttributeDict): + def __init__(self, min_age: int, allowed_regions: list[str]) -> None: + super().__init__({ + "min_age": min_age, + "allowed_regions": allowed_regions + }) + +@injectable +class RegionalAgePolicy(PolicyWithRequirement): + requirement_type = RegionRequirement + + async def handle(self, context: IExecutionContext, requirement: RegionRequirement) -> bool: + user_age = int(context.user.get("age", 0)) + user_region = context.user.get("region", "") + + age_requirement_met = user_age >= requirement.min_age + region_requirement_met = user_region in requirement.allowed_regions + + return age_requirement_met and region_requirement_met + +@Controller("/regional") +@Authorize() +@AuthenticationRequired() +class RegionalController: + @get("/content") + @CheckPolicies( + RegionalAgePolicy[RegionRequirement(21, ["US", "CA", "UK"])] + ) + async def regional_content(self): + return "Region-Restricted Content" + + @get("/special") + @CheckPolicies( + RegionalAgePolicy[RegionRequirement(18, ["US"])] | + RegionalAgePolicy[RegionRequirement(21, ["CA", "UK"])] + ) + async def special_content(self): + return "Special Content with Different Regional Requirements" +``` + +## Best Practices + +1. Use descriptive names for policy and requirement classes +2. Keep requirement parameters simple and type-safe +3. Document the expected format and values of requirements +4. Use custom requirement types for complex parameter sets +5. Consider combining policies for complex authorization scenarios +6. Handle edge cases and invalid requirement values gracefully + +## Combining with Other Policy Types + +Policies with requirements can be combined with other policy types using logical operators: + +```python +@Controller("/mixed") +@Authorize() +@AuthenticationRequired() +class MixedController: + @get("/special") + @CheckPolicies( + AgeRequirementPolicy[21] & + RolePolicy("premium_user") + ) + async def special_access(self): + return "Special Access Content" + + @get("/alternative") + @CheckPolicies( + (AgeRequirementPolicy[18] & TeamMemberPolicy["beta-testers"]) | + RolePolicy("admin") + ) + async def alternative_access(self): + return "Alternative Access Content" +``` + +For more information about combining different types of policies, see [Combining Policies](./combining-policies.md). diff --git a/docs/security/authorization/policies.md b/docs/security/authorization/policies.md new file mode 100644 index 00000000..3f391504 --- /dev/null +++ b/docs/security/authorization/policies.md @@ -0,0 +1,79 @@ +# Policies in Ellar + +Policies are rules that determine whether a user can perform a specific action or access a particular resource. In Ellar, policies are implemented as classes that inherit from the `Policy` base class. + +## Basic Policy Structure + +A basic policy must inherit from `Policy` and implement the `handle` method: + +```python +from ellar.auth import Policy +from ellar.common import IExecutionContext +from ellar.di import injectable + +@injectable +class AdultOnlyPolicy(Policy): + async def handle(self, context: IExecutionContext) -> bool: + # Access user information from context + user_age = int(context.user.get("age", 0)) + return user_age >= 18 +``` + +## Using Policies + +To apply policies to your routes or controllers, use the `@CheckPolicies` decorator: + +```python +from ellar.auth import AuthenticationRequired, Authorize, CheckPolicies +from ellar.common import Controller, get + +@Controller("/content") +@Authorize() +class ContentController: + @get("/adult") + @CheckPolicies(AdultOnlyPolicy) + async def adult_content(self): + return "Adult Only Content" +``` + +## Policy Context + +The `IExecutionContext` passed to the policy's `handle` method provides access to: + +- User information (`context.user`) +- Request details +- Application context +- Service provider for dependency injection + +## Best Practices + +1. Always decorate policy classes with `@injectable` for proper dependency injection +2. Keep policies focused on a single responsibility +3. Make policy names descriptive of their function +4. Use type hints for better code maintainability +5. Return boolean values from the `handle` method + +## Example: Premium User Policy + +Here's an example of a policy that checks if a user has premium access: + +```python +@injectable +class PremiumUserPolicy(Policy): + async def handle(self, context: IExecutionContext) -> bool: + subscription_type = context.user.get("subscription", "free") + return subscription_type == "premium" + +@Controller("/premium") +@Authorize() +class PremiumController: + @get("/content") + @CheckPolicies(PremiumUserPolicy) + async def premium_content(self): + return "Premium Content" +``` + +For more specific types of policies, see: +- [Role-Based Authorization](./role-based.md) +- [Claims-Based Authorization](./claims-based.md) +- [Custom Policies with Requirements](./custom-policies.md) diff --git a/docs/security/authorization/role-based.md b/docs/security/authorization/role-based.md new file mode 100644 index 00000000..f79d657c --- /dev/null +++ b/docs/security/authorization/role-based.md @@ -0,0 +1,102 @@ +# Role-Based Authorization + +Role-based authorization is a common approach where access to resources is determined by the roles assigned to a user. Ellar provides built-in support for role-based authorization through the `RolePolicy` class. + +## Using RolePolicy + +The `RolePolicy` class allows you to check if a user has specific roles: + +```python +from ellar.auth import AuthenticationRequired, Authorize, CheckPolicies +from ellar.auth.policy import RolePolicy +from ellar.common import Controller, get + +@Controller("/admin") +@Authorize() +class AdminController: + @get("/dashboard") + @CheckPolicies(RolePolicy("admin")) + async def admin_dashboard(self): + return "Admin Dashboard" + + @get("/reports") + @CheckPolicies(RolePolicy("admin", "analyst")) # Requires both roles + async def view_reports(self): + return "Admin Reports" +``` + +## How RolePolicy Works + +The `RolePolicy` checks the user's roles against the required roles. The roles are typically stored in the user's claims under the "roles" key: + +```python +# Example user data structure +user_data = { + "id": "123", + "username": "john_doe", + "roles": ["admin", "user"] +} +``` + +## Multiple Role Requirements + +You can require multiple roles in different ways: + +```python +@Controller("/organization") +@Authorize() +class OrganizationController: + @get("/finance") + @CheckPolicies(RolePolicy("admin") | RolePolicy("finance")) # Requires either role + async def finance_dashboard(self): + return "Finance Dashboard" + + @get("/hr-admin") + @CheckPolicies(RolePolicy("hr") & RolePolicy("admin")) # Requires both roles + async def hr_admin_dashboard(self): + return "HR Admin Dashboard" + + @get("/super-admin") + @CheckPolicies(RolePolicy("admin", "super")) # Requires both roles (alternative syntax) + async def super_admin_dashboard(self): + return "Super Admin Dashboard" +``` + +## Best Practices + +1. Use role names that are descriptive and follow a consistent naming convention +2. Keep the number of roles manageable +3. Consider using claims for more fine-grained permissions +4. Use role combinations when more complex access rules are needed + +## Example: Multi-Department Access + +Here's an example showing how to handle access for users with different department roles: + +```python +@Controller("/departments") +@Authorize() +class DepartmentController: + @get("/it") + @CheckPolicies(RolePolicy("it_staff") | RolePolicy("it_manager")) + async def it_department(self): + return "IT Department" + + @get("/it/admin") + @CheckPolicies(RolePolicy("it_manager")) + async def it_admin(self): + return "IT Administration" + + @get("/cross-department") + @CheckPolicies( + RolePolicy("it_manager") | + RolePolicy("hr_manager") | + RolePolicy("finance_manager") + ) + async def department_managers(self): + return "Department Managers Only" +``` + +## Combining with Other Policies + +Role-based authorization can be combined with other policy types for more complex authorization scenarios. See [Combining Policies](./combining-policies.md) for more information. diff --git a/ellar/auth/decorators.py b/ellar/auth/decorators.py index c72e98ef..57c199da 100644 --- a/ellar/auth/decorators.py +++ b/ellar/auth/decorators.py @@ -11,10 +11,28 @@ def CheckPolicies(*policies: t.Union[str, PolicyType]) -> t.Callable: """ - ========= CONTROLLER AND ROUTE FUNCTION DECORATOR ============== - Decorates a controller or a route function with specific policy requirements - :param policies: - :return: + Applies policy requirements to a controller or route function. + + This decorator allows you to specify one or more policies that must be satisfied + for the user to access the decorated endpoint. Policies can be either string + identifiers or PolicyType objects. + + Example: + ```python + @Controller() + class UserController: + @CheckPolicies('admin', RequireRole('manager')) + def get_sensitive_data(self): + return {'data': 'sensitive information'} + ``` + + Args: + *policies: Variable number of policy requirements. Can be strings or PolicyType objects. + - String policies are resolved using the policy provider + - PolicyType objects are evaluated directly + + Returns: + A decorator function that applies the policy requirements. """ def _decorator(target: t.Callable) -> t.Union[t.Callable, t.Any]: @@ -26,9 +44,25 @@ def _decorator(target: t.Callable) -> t.Union[t.Callable, t.Any]: def Authorize() -> t.Callable: """ - ========= CONTROLLER AND ROUTE FUNCTION DECORATOR ============== - Decorates a controller class or route function with `AuthorizationInterceptor` - :return: + Enables authorization checks for a controller or route function. + + This decorator adds the AuthorizationInterceptor which performs two main checks: + 1. Verifies that the user is authenticated + 2. Validates any policy requirements specified using @CheckPolicies + + Example: + ```python + @Controller() + @Authorize() # Enable authorization for all routes in controller + class SecureController: + @get('/') + @CheckPolicies('admin') # Require admin policy + def secure_endpoint(self): + return {'message': 'secure data'} + ``` + + Returns: + A decorator function that enables authorization checks. """ return set_meta(constants.ROUTE_INTERCEPTORS, [AuthorizationInterceptor]) @@ -39,13 +73,34 @@ def AuthenticationRequired( openapi_scope: t.Optional[t.List] = None, ) -> t.Callable: """ - ========= CONTROLLER AND ROUTE FUNCTION DECORATOR ============== - - Decorates a controller class or route function with `IsAuthenticatedGuard` - - @param authentication_scheme: authentication_scheme - Based on the authentication scheme class name or openapi_name used. - @param openapi_scope: OpenAPi scope - @return: Callable + Requires authentication for accessing a controller or route function. + + This decorator adds the AuthenticatedRequiredGuard which ensures that requests + are authenticated before they can access the protected resource. + + Example: + ```python + @Controller() + class UserController: + @get('/profile') + @AuthenticationRequired('jwt') # Require JWT authentication + def get_profile(self): + return {'user': 'data'} + + @get('/public') + @AuthenticationRequired(openapi_scope=['read:public']) + def public_data(self): + return {'public': 'data'} + ``` + + Args: + authentication_scheme: Optional name of the authentication scheme to use. + This should match the scheme name defined in your authentication setup. + openapi_scope: Optional list of OpenAPI security scopes required for the endpoint. + These scopes will be reflected in the OpenAPI documentation. + + Returns: + A decorator function that enforces authentication requirements. """ if callable(authentication_scheme): return set_meta(constants.GUARDS_KEY, [AuthenticatedRequiredGuard(None, [])])( @@ -60,9 +115,28 @@ def AuthenticationRequired( def SkipAuth() -> t.Callable: """ - ========= CONTROLLER AND ROUTE FUNCTION DECORATOR ============== - Decorates a Class or Route Function with SKIP_AUTH attribute that is checked by `AuthenticationRequiredGuard` - @return: Callable + Marks a controller or route function to skip authentication checks. + + This decorator is useful when you have a controller with @AuthenticationRequired + but want to exclude specific routes from the authentication requirement. + + Example: + ```python + @Controller() + @AuthenticationRequired() # Require auth for all routes + class UserController: + @get('/private') + def private_data(self): # This requires auth + return {'private': 'data'} + + @get('/public') + @SkipAuth() # This endpoint skips auth + def public_data(self): + return {'public': 'data'} + ``` + + Returns: + A decorator function that marks the endpoint to skip authentication. """ return set_meta( diff --git a/ellar/auth/guards/auth_required.py b/ellar/auth/guards/auth_required.py index 624c0a45..aa479fe9 100644 --- a/ellar/auth/guards/auth_required.py +++ b/ellar/auth/guards/auth_required.py @@ -9,6 +9,10 @@ class AuthenticatedRequiredGuard(GuardCanActivate): + """ + This guard will check if the user is authenticated and also allow you to define the authentication scheme and openapi scope. + """ + status_code = starlette.status.HTTP_401_UNAUTHORIZED def __init__( diff --git a/ellar/auth/policy/base.py b/ellar/auth/policy/base.py index 06a8517f..fdad6426 100644 --- a/ellar/auth/policy/base.py +++ b/ellar/auth/policy/base.py @@ -70,7 +70,7 @@ async def handle(self, context: IExecutionContext, requirement: t.Any) -> bool: def __class_getitem__(cls, parameters: t.Any) -> "Policy": _parameters = parameters if isinstance(parameters, tuple) else (parameters,) - hash_id = hash(_parameters) + hash_id = hash((id(cls), _parameters)) if hash_id not in cls.__requirements__: cls.__requirements__[hash_id] = _PolicyHandlerWithRequirement( cls, cls.requirement_type(*_parameters)