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
:
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:
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:
@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:
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:
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¶
- Use descriptive names for policy and requirement classes
- Keep requirement parameters simple and type-safe
- Document the expected format and values of requirements
- Use custom requirement types for complex parameter sets
- Consider combining policies for complex authorization scenarios
- 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:
@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.