Web Authorization¶
Once authentication has answered who is visiting, authorization answers what they're allowed to see and do. Web authorization works exactly like the API: it's built around the Guard and flat permission scopes, and your routes think in terms of scopes, never role names directly.
The Guard¶
Guard is Uvicore's authorization dependency. It wraps FastAPI's Security model and checks the current request.user against the scopes a route requires.
from uvicore.http.routing import Guard
You rarely instantiate it yourself, the shorthands below do that for you.
Three Ways to Guard a Route¶
The scopes= shorthand (simplest and preferred):
@route.get('/dashboard', name='dashboard', scopes=['authenticated'])
async def dashboard(request: Request):
return await response.View('wiki/dashboard.j2', {'request': request})
The auth= shorthand:
@route.get('/admin', name='admin', auth=Guard(['authenticated', 'admin']))
async def admin(request: Request):
return await response.View('wiki/admin.j2', {'request': request})
The full middleware= list:
@route.get('/reports', name='reports', middleware=[Guard(['authenticated', 'reports.read'])])
async def reports(request: Request):
return await response.View('wiki/reports.j2', {'request': request})
Injecting the User¶
Use Guard as an endpoint parameter and the route is protected and the authorized user is handed straight to you.
from uvicore.auth import UserInfo
@route.get('/profile', name='profile')
async def profile(request: Request, user: UserInfo = Guard(['authenticated'])):
return await response.View('wiki/profile.j2', {'request': request, 'user': user})
Where Guards Can Live¶
Apply authorization at any level, Uvicore merges the requirements so they accumulate from parent to child:
# Controller-wide
@uvicore.controller()
class AdminPanel(Controller):
scopes = ['authenticated', 'admin']
# Group-wide
@route.group('/admin', scopes=['authenticated', 'admin'])
def admin():
route.controller('dashboard')
# Endpoint-specific (adds to controller/group scopes)
@route.delete('/users/{id}', name='user.delete', scopes=['authenticated', 'users.delete'])
async def delete_user(request: Request, id: int):
...
How Scopes Are Evaluated¶
Guard checks permissions with three simple rules:
- If a route requires no scopes, access is allowed.
- If the user is a superadmin, access is always allowed.
- Otherwise, the user must hold all the required scopes.
Note
This is an AND check, not OR. scopes=['posts.read', 'posts.delete'] means the user needs both.
Authenticated vs Authorized¶
Uvicore distinguishes two failure modes:
- Not authenticated - the visitor is anonymous and the route requires access. For web routes this typically redirects to your login page (if configured), otherwise raises
NotAuthenticated. - Authenticated but unauthorized - the user is logged in but missing a required scope →
PermissionDenied.
Checking Permissions in Templates¶
Show or hide UI based on what the current user can do. UserInfo exposes can(), can_any() and is_authenticated, and you can always inspect the flat permissions list directly.
{% if request.user.can('reports.read') %}
<a href="{{ url('wiki.reports') }}">View Reports</a>
{% endif %}
{% if 'admin' in request.user.permissions %}
<a href="{{ url('wiki.admin') }}">Admin</a>
{% endif %}
Authorization tips
- Guard with scopes, not hardcoded role names.
- Put shared rules at the controller or group level; reserve endpoint scopes for the specific cases.
- Treat
superadminas an emergency bypass, not your primary authorization model. - Check permissions in templates to conditionally render UI, but still guard the route itself, never rely on a hidden link alone.