Authorization
Introduction
Section titled “Introduction”Authentication answers who the user is. Authorization answers what they
may do. Almasix ships Laravel-shaped Gates and Policies on almasix.auth:
from almasix.auth import Gate, Policy, authorizeRegister abilities in a provider boot() method (typically
app/providers/app_service_provider.py) after the kernel boots. Checks are
synchronous so they work in controllers, Form Requests, and Prism templates.
Where the files live
Section titled “Where the files live”| Role | Path |
|---|---|
| Register gates / policies | app/providers/app_service_provider.py (boot()) |
User model (can / cannot) |
app/models/user.py — AuthenticatableMixin already includes Authorizable |
| Policy classes | app/policies/<model>_policy.py (created by smith make:policy) |
| Models | app/models/<model>.py |
| Controllers | app/http/controllers/… |
| Form requests | app/http/requests/… |
Route can middleware |
routes/web.py / routes/api.py |
Prism @can |
resources/views/….prism.html |
Alias can |
bootstrap/app.py (installer wires can → Authorize) |
A gate is a named closure. Define it in a provider — not at import time on
a model module — so the bound Gate façade exists:
from almasix.auth import Gatefrom almasix.providers.provider import ServiceProvider
class AppServiceProvider(ServiceProvider): def boot(self) -> None: Gate.define("view-dashboard", lambda user: user is not None) Gate.define("update-post", lambda user, post: user.id == post.user_id)The first argument is always the user (or None for guests).
Allowing guests
Section titled “Allowing guests”If the user parameter is optional, guests may pass:
def view_post(user=None, post=None): return post is not None and post.published
Gate.define("view-post", view_post)A required user parameter denies guests without calling the callback.
user: User | None, user: Optional[User], or user=None all count as optional.
Checking abilities
Section titled “Checking abilities”if Gate.allows("update-post", post): ...
Gate.denies("update-post", post)Gate.check(["update-post", "delete-post"], post) # all must passGate.any(["update-post", "delete-post"], post)Gate.none(["update-post", "delete-post"], post)
Gate.authorize("update-post", post) # raises AuthorizationException (403)response = Gate.inspect("update-post", post) # AuthorizationResponseHelpers gate() and authorize() mirror the façade.
Callbacks may be a lambda, a handle() class, a "pkg.mod.Class@method"
string, or [PolicyClass, "method"].
Another user
Section titled “Another user”Gate.for_user(other).allows("update-post", post)Intercepting checks
Section titled “Intercepting checks”Gate.before(lambda user, ability: True if getattr(user, "admin", False) else None)Gate.after(lambda user, ability, result, arguments: result)before / after returning non-None win. Admins typically return True from
before.
Gate.default_deny_response(...) customizes the message/status when a check
returns False / None.
Authorizable users
Section titled “Authorizable users”AuthenticatableMixin includes Authorizable. You do not register this
separately — any user loaded by the auth guard already has:
user.can("update-post", post)user.cannot("update-post", post)user.cant("update-post", post) # alias of cannotuser.can_any(["update", "delete"], post)user.canany(["update", "delete"], post)Those methods call Gate.for_user(self) so they do not depend on the current
request user.
Policies
Section titled “Policies”Policies group abilities for a model. Generate a stub:
smith make:policy PostPolicy --model=Post --resource# writes app/policies/post_policy.py--resource (implied when --model is set) stubs view_any, view, create,
update, delete, restore, and force_delete.
from almasix.auth import Policyfrom app.models.post import Post
class PostPolicy(Policy): def before(self, user, ability): if getattr(user, "admin", False): return True return None
def view_any(self, user) -> bool: return user is not None
def update(self, user, post: Post) -> bool: return user.id == post.user_id
def delete(self, user, post: Post): if user.id == post.user_id: return True return self.deny("You do not own this post.")Policy includes allow, deny, deny_with_status, and deny_as_not_found
(HTTP 404).
Registering policies
Section titled “Registering policies”Explicit (recommended in boot()):
from almasix.auth import Gatefrom app.models.post import Postfrom app.policies.post_policy import PostPolicy
class AppServiceProvider(ServiceProvider): def boot(self) -> None: Gate.policy(Post, PostPolicy) # or many at once (Laravel AuthServiceProvider::$policies): Gate.register_policies({ Post: PostPolicy, })Auto-discovery (if you skip Gate.policy)
Section titled “Auto-discovery (if you skip Gate.policy)”When Almasix authorizes against a model and no mapping exists, it guesses a policy class, in order:
-
The model’s
policyattribute, if it is a class:class Post: policy = PostPolicy -
Custom guessers from
Gate.guess_policy_names_using(...) -
Default paths from the model name / module:
Model Guessed import Postapp.policies.post_policy.PostPolicyapp.models.post.Postapp.policies.post_policy.PostPolicyapp.models.blog.post.Postapp.policies.blog.post_policy.PostPolicyandapp.policies.blog.post.PostPolicy
Guessing imports that class if it exists. There is no directory scanner — put the policy on the guessed path, or register it explicitly.
Gate.guess_policy_names_using replaces the default guesser (it does not
append). Return a class, an import string, a list of either, or None:
Gate.guess_policy_names_using( lambda model: f"app.policies.{model.__name__.lower()}_policy.{model.__name__}Policy")Custom policy methods
Section titled “Custom policy methods”Any method on the policy is an ability. There is no extra registration step.
-
Add the method to
app/policies/post_policy.py:def publish(self, user, post: Post) -> bool:return user.id == post.user_id and not post.publisheddef assign_editor(self, user, post: Post, editor) -> bool:return user.admin -
Call it by method name (snake or camelCase) against an instance or class:
Gate.allows("publish", post)user.can("publish", post)self.authorize("publish", post)Gate.allows("assign_editor", [post, editor]) -
Optional HTTP wiring:
routes/web.py Route.post("/posts/{post}/publish", [PostController, "publish"]).can("publish", "post")# controllerclass PostController(Controller):async def publish(self, post: Post):self.authorize("publish", post)# form requestclass PublishPostRequest(FormRequest):def authorize(self):return Gate.allows("publish", self.route("post"))@can('publish', post)<button>Publish</button>@endcan
Resource actions (index/show/store/update/destroy) still map to the
standard CRUD names; custom methods are only used when you authorize that name.
Authorize against an instance (update, publish) or a class
(create / view_any):
Gate.allows("update", post)Gate.allows("create", Post)Resource gates
Section titled “Resource gates”Named abilities on a policy without going through model matching:
Gate.resource("posts", PostPolicy)Gate.allows("posts.update", post)Gate.resource("posts", PostPolicy, abilities={"publish": "publish"})Gate.allows("posts.publish", post)Controllers and Form Requests
Section titled “Controllers and Form Requests”Controllers inherit authorize / authorize_for_user / can / cannot /
authorize_when / authorize_unless:
class PostController(Controller): async def update(self, post: Post): self.authorize("update", post) ...
async def publish(self, post: Post): self.authorize_when(post.draft, "publish", post)Set authorizes_resource = Post (or (Post, "post")) to map resource actions
(index → view_any, show → view, store → create, update →
update, destroy → delete) before the action runs.
Form Requests may return a bool or an AuthorizationResponse:
class UpdatePostRequest(FormRequest): def authorize(self): return Gate.allows("update", self.route("post"))False still raises 403 as in M3.
Route middleware
Section titled “Route middleware”The can alias is registered in bootstrap/app.py:
Route.put("/posts/{post}", [PostController, "update"]).can("update", "post")# equivalent: middleware=["can:update,post"]Pass a class path for class-based abilities: can:create,app.models.post.Post.
@can('update', post) <a href="/posts/edit">Edit</a>@else <span>Read only</span>@endcan
@cannot('delete', post) ...@endcannot
@canany(['update', 'delete'], post) ...@endcanany@cannotany is the inverse of @canany.
Testing
Section titled “Testing”from almasix.auth import Gate, AuthorizationException
Gate.flush()Gate.define("ping", lambda user: True)assert Gate.for_user(user).allows("ping")AuthorizationException is an HttpException (403, or 404 when a policy
uses deny_as_not_found).