fastapi-guardian 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-guardian
3
+ Version: 0.1.0
4
+ Summary: Flexible permissions for FastAPI
5
+ Keywords: fastapi,authorization,security
6
+ Author: achopik
7
+ Author-email: achopik <artikchopik@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Requires-Dist: fastapi>=0.130.0
15
+ Requires-Dist: lark>=1.3.1
16
+ Requires-Dist: pydantic>=2.12.0
17
+ Requires-Dist: sqlalchemy>=2.0.10 ; extra == 'sqlalchemy'
18
+ Requires-Dist: tortoise-orm>=1.1.0 ; extra == 'tortoise'
19
+ Requires-Python: >=3.14
20
+ Project-URL: Repository, https://github.com/achopik/fastapi-guardian
21
+ Provides-Extra: sqlalchemy
22
+ Provides-Extra: tortoise
23
+ Description-Content-Type: text/markdown
24
+
25
+ # fastapi-guardian
26
+
27
+ ```fastapi-guardian``` is a WIP Python library created for flexible permission management in FastAPI applications. It provides a generic engine and ORM bindings to perform access checks and DB-level filtering operations.
28
+
29
+ ## Features
30
+
31
+ - Generic engine for permission management
32
+ - SQLAlchemy and Tortoise ORM bindings for access filtering
33
+ - Database-agnostic core decision engine
34
+ - Expression mini-DSL for permission conditions (AND, OR, NOT) with custom dev-defined predicates (e.g, 'self', 'only_drafts')
35
+ - 3 scopes of permissions: global, resource-based (access to specific resource instance denoted by ID) and conditional (access to specific resource instance based on custom conditions defined in application code)
36
+ - FastAPI-native dependency injection via `Permission` dependency.
37
+ - Fully typed library definitions, especially user-facing interfaces.
38
+ - 🚧 Minimal AI involvement and fully covered with tests.
39
+
40
+
41
+ ## Supported Python version
42
+
43
+ Currently, library is designed to work with Python 3.14+
44
+
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install fastapi-guardian
50
+ ```
51
+
52
+ ## Quickstart
53
+
54
+ ```py
55
+ import enum
56
+ import typing
57
+
58
+ import uvicorn
59
+ from fastapi import Depends, FastAPI, HTTPException, Request, status
60
+ from pydantic import BaseModel
61
+ from sqlalchemy.engine import create_engine
62
+ from sqlalchemy.orm import (
63
+ DeclarativeBase,
64
+ Mapped,
65
+ Session,
66
+ mapped_column,
67
+ relationship,
68
+ sessionmaker,
69
+ )
70
+ from sqlalchemy.schema import ForeignKey
71
+ from sqlalchemy.sql import select
72
+ from sqlalchemy.types import JSON, Integer, String
73
+ from starlette.middleware.sessions import SessionMiddleware
74
+
75
+ from fastapi_guardian.dependencies import BasePermission
76
+ from fastapi_guardian.dto import AuthContext, Principal
77
+ from fastapi_guardian.ext.sqlalchemy import SqlalchemyAuthEngine, SqlalchemyResource
78
+
79
+
80
+
81
+ # 1. Define your sqlalchemy models, inherit SqlalchemyResource
82
+ class Base(DeclarativeBase, SqlalchemyResource, __resource_abstract__=True):
83
+ __resource_app_name__ = "example"
84
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
85
+
86
+
87
+ class Role(Base):
88
+ __tablename__ = "roles"
89
+
90
+ name: Mapped[str] = mapped_column(String(255))
91
+ permission_grants: Mapped[list["RolePermissionGrant"]] = relationship(
92
+ "RolePermissionGrant", back_populates="role"
93
+ )
94
+ users: Mapped[list["UserRole"]] = relationship("UserRole", back_populates="role")
95
+
96
+
97
+ class RolePermissionGrant(Base):
98
+ __tablename__ = "role_permission_grants"
99
+
100
+ role_id: Mapped[int] = mapped_column(Integer, ForeignKey("roles.id"))
101
+ role: Mapped["Role"] = relationship("Role", back_populates="permission_grants")
102
+ resource: Mapped[str] = mapped_column(String(255))
103
+ action: Mapped[str] = mapped_column(String(255))
104
+ scope: Mapped[str] = mapped_column(String(255))
105
+ extra: Mapped[dict] = mapped_column(JSON)
106
+
107
+
108
+ class User(Base):
109
+ __tablename__ = "users"
110
+
111
+ username: Mapped[str] = mapped_column(String(255))
112
+ email: Mapped[str] = mapped_column(String(255))
113
+ password: Mapped[str] = mapped_column(String(255))
114
+
115
+ articles: Mapped[list["Article"]] = relationship("Article", back_populates="author")
116
+ roles: Mapped[list["UserRole"]] = relationship("UserRole", back_populates="user")
117
+
118
+
119
+ class Article(Base):
120
+ __tablename__ = "articles"
121
+
122
+ title: Mapped[str] = mapped_column(String(255))
123
+ content: Mapped[str] = mapped_column(String(255))
124
+ author_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
125
+ author: Mapped["User"] = relationship("User", back_populates="articles")
126
+ category: Mapped[str] = mapped_column(String(255))
127
+
128
+
129
+
130
+ # 2. Create your auth engine
131
+ auth_engine = SqlalchemyAuthEngine[Base]()
132
+
133
+
134
+ # 3. Create your permission dependency class and authentication logic
135
+ def get_authorized_principal(
136
+ request: Request, db: Session = Depends(get_db)
137
+ ) -> Principal[int] | None:
138
+ user_id = request.session.get("user_id")
139
+ if user_id is None:
140
+ return None # Alternatively, you can raise an HTTPException here, but None is handled by permission itself
141
+ user = db.query(User).filter(User.id == user_id).first()
142
+ if user is None:
143
+ return None
144
+
145
+ permissions = (
146
+ db.query(RolePermissionGrant)
147
+ .join(Role, Role.id == RolePermissionGrant.role_id)
148
+ .join(UserRole, UserRole.role_id == Role.id)
149
+ .filter(UserRole.user_id == user_id)
150
+ .all()
151
+ )
152
+
153
+ return Principal(
154
+ id=user.id,
155
+ email=user.email,
156
+ username=user.username,
157
+ permissions=[
158
+ {
159
+ "resource": permission.resource,
160
+ "action": permission.action,
161
+ "scope": permission.scope,
162
+ **permission.extra,
163
+ }
164
+ for permission in permissions
165
+ ],
166
+ )
167
+
168
+
169
+ class AppPermission[T: type[Base]](BasePermission[T, int]):
170
+ auth_engine = auth_engine
171
+
172
+ async def __call__(self, principal: typing.Annotated[Principal, Depends(get_authorized_principal)]):
173
+ return await self.authorize(principal=principal)
174
+
175
+
176
+ # Note: You can use any string value as an action, enum prefered here for typed suggestions and consistency across the application.
177
+ class AuthAction(enum.StrEnum):
178
+ READ = "read"
179
+ CREATE = "create"
180
+ UPDATE = "update"
181
+ DELETE = "delete"
182
+
183
+
184
+ # 4. Define your API endpoints
185
+ @app.get("/users")
186
+ async def get_users(
187
+ auth_ctx: AuthContext = Depends(
188
+ AppPermission(
189
+ resource=User,
190
+ action=AuthAction.READ,
191
+ scopes=["global", "resource", "conditional"],
192
+ predicates=[
193
+ {
194
+ "name": "self",
195
+ "fn": lambda ctx: ctx.resource.id == ctx.principal.id,
196
+ "description": "Allow access to own user resource",
197
+ },
198
+ ],
199
+ )
200
+ ),
201
+ db: Session = Depends(get_db),
202
+ ) -> list[UserDto]:
203
+ query = select(User)
204
+ query = auth_engine.filter_query(context=auth_ctx, query=query)
205
+ users = db.execute(query).scalars().all()
206
+ return [
207
+ UserDto(id=user.id, username=user.username, email=user.email) for user in users
208
+ ]
209
+
210
+
211
+ @app.post("/users")
212
+ async def create_user(
213
+ body: UserCreateDto,
214
+ # By default, permission assumes only global scope, which is the case for most of CREATE actions
215
+ _auth: AuthContext = Depends(
216
+ AppPermission(resource=User, action=AuthAction.CREATE)
217
+ ),
218
+ db: Session = Depends(get_db),
219
+ ) -> UserDto:
220
+ user = User(username=body.username, email=body.email, password=body.password)
221
+ db.add(user)
222
+ db.commit()
223
+ db.refresh(user)
224
+ return UserDto(id=user.id, username=user.username, email=user.email)
225
+
226
+
227
+ @app.get("/articles")
228
+ async def get_articles(
229
+ auth_ctx: AuthContext = Depends(
230
+ AppPermission(
231
+ resource=Article,
232
+ action=AuthAction.READ,
233
+ scopes=["global", "resource", "conditional"],
234
+ predicates=[
235
+ {
236
+ "name": "self",
237
+ "fn": lambda ctx: ctx.resource.author_id == ctx.principal.id,
238
+ "description": "Allow access to own articles",
239
+ },
240
+ {
241
+ "name": "only_published",
242
+ "fn": lambda ctx: ctx.resource.category == "published",
243
+ "description": "Allow access to published articles only",
244
+ },
245
+ ],
246
+ )
247
+ ),
248
+ db: Session = Depends(get_db),
249
+ ) -> list[ArticleDto]:
250
+ query = select(Article)
251
+ # Note: Apply filter to query manually here. Expression will be added as AND statement to the existing filters.
252
+ query = auth_engine.filter_query(context=auth_ctx, query=query)
253
+ articles = db.execute(query).scalars().all()
254
+ return [
255
+ ArticleDto(
256
+ id=article.id,
257
+ title=article.title,
258
+ content=article.content,
259
+ author_id=article.author_id,
260
+ category=article.category,
261
+ )
262
+ for article in articles
263
+ ]
264
+
265
+ # 5. Create and store permission grants somewhere (completely up to you, check examples for to-go model definitions, auth engine only cares about Principal DTO):
266
+ grant = RolePermissionGrant(
267
+ role_id=author_role.id,
268
+ resource=Article.__resource_code__,
269
+ action=AuthAction.READ,
270
+ scope="conditional",
271
+ # Notice that we can use expression mini-DSL here to build complex conditions.
272
+ extra={"condition": "self or only_published"},
273
+ )
274
+ ```
275
+
276
+ For detailed ready-to-run examples, see [examples](examples) directory.
@@ -0,0 +1,252 @@
1
+ # fastapi-guardian
2
+
3
+ ```fastapi-guardian``` is a WIP Python library created for flexible permission management in FastAPI applications. It provides a generic engine and ORM bindings to perform access checks and DB-level filtering operations.
4
+
5
+ ## Features
6
+
7
+ - Generic engine for permission management
8
+ - SQLAlchemy and Tortoise ORM bindings for access filtering
9
+ - Database-agnostic core decision engine
10
+ - Expression mini-DSL for permission conditions (AND, OR, NOT) with custom dev-defined predicates (e.g, 'self', 'only_drafts')
11
+ - 3 scopes of permissions: global, resource-based (access to specific resource instance denoted by ID) and conditional (access to specific resource instance based on custom conditions defined in application code)
12
+ - FastAPI-native dependency injection via `Permission` dependency.
13
+ - Fully typed library definitions, especially user-facing interfaces.
14
+ - 🚧 Minimal AI involvement and fully covered with tests.
15
+
16
+
17
+ ## Supported Python version
18
+
19
+ Currently, library is designed to work with Python 3.14+
20
+
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install fastapi-guardian
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ ```py
31
+ import enum
32
+ import typing
33
+
34
+ import uvicorn
35
+ from fastapi import Depends, FastAPI, HTTPException, Request, status
36
+ from pydantic import BaseModel
37
+ from sqlalchemy.engine import create_engine
38
+ from sqlalchemy.orm import (
39
+ DeclarativeBase,
40
+ Mapped,
41
+ Session,
42
+ mapped_column,
43
+ relationship,
44
+ sessionmaker,
45
+ )
46
+ from sqlalchemy.schema import ForeignKey
47
+ from sqlalchemy.sql import select
48
+ from sqlalchemy.types import JSON, Integer, String
49
+ from starlette.middleware.sessions import SessionMiddleware
50
+
51
+ from fastapi_guardian.dependencies import BasePermission
52
+ from fastapi_guardian.dto import AuthContext, Principal
53
+ from fastapi_guardian.ext.sqlalchemy import SqlalchemyAuthEngine, SqlalchemyResource
54
+
55
+
56
+
57
+ # 1. Define your sqlalchemy models, inherit SqlalchemyResource
58
+ class Base(DeclarativeBase, SqlalchemyResource, __resource_abstract__=True):
59
+ __resource_app_name__ = "example"
60
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
61
+
62
+
63
+ class Role(Base):
64
+ __tablename__ = "roles"
65
+
66
+ name: Mapped[str] = mapped_column(String(255))
67
+ permission_grants: Mapped[list["RolePermissionGrant"]] = relationship(
68
+ "RolePermissionGrant", back_populates="role"
69
+ )
70
+ users: Mapped[list["UserRole"]] = relationship("UserRole", back_populates="role")
71
+
72
+
73
+ class RolePermissionGrant(Base):
74
+ __tablename__ = "role_permission_grants"
75
+
76
+ role_id: Mapped[int] = mapped_column(Integer, ForeignKey("roles.id"))
77
+ role: Mapped["Role"] = relationship("Role", back_populates="permission_grants")
78
+ resource: Mapped[str] = mapped_column(String(255))
79
+ action: Mapped[str] = mapped_column(String(255))
80
+ scope: Mapped[str] = mapped_column(String(255))
81
+ extra: Mapped[dict] = mapped_column(JSON)
82
+
83
+
84
+ class User(Base):
85
+ __tablename__ = "users"
86
+
87
+ username: Mapped[str] = mapped_column(String(255))
88
+ email: Mapped[str] = mapped_column(String(255))
89
+ password: Mapped[str] = mapped_column(String(255))
90
+
91
+ articles: Mapped[list["Article"]] = relationship("Article", back_populates="author")
92
+ roles: Mapped[list["UserRole"]] = relationship("UserRole", back_populates="user")
93
+
94
+
95
+ class Article(Base):
96
+ __tablename__ = "articles"
97
+
98
+ title: Mapped[str] = mapped_column(String(255))
99
+ content: Mapped[str] = mapped_column(String(255))
100
+ author_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
101
+ author: Mapped["User"] = relationship("User", back_populates="articles")
102
+ category: Mapped[str] = mapped_column(String(255))
103
+
104
+
105
+
106
+ # 2. Create your auth engine
107
+ auth_engine = SqlalchemyAuthEngine[Base]()
108
+
109
+
110
+ # 3. Create your permission dependency class and authentication logic
111
+ def get_authorized_principal(
112
+ request: Request, db: Session = Depends(get_db)
113
+ ) -> Principal[int] | None:
114
+ user_id = request.session.get("user_id")
115
+ if user_id is None:
116
+ return None # Alternatively, you can raise an HTTPException here, but None is handled by permission itself
117
+ user = db.query(User).filter(User.id == user_id).first()
118
+ if user is None:
119
+ return None
120
+
121
+ permissions = (
122
+ db.query(RolePermissionGrant)
123
+ .join(Role, Role.id == RolePermissionGrant.role_id)
124
+ .join(UserRole, UserRole.role_id == Role.id)
125
+ .filter(UserRole.user_id == user_id)
126
+ .all()
127
+ )
128
+
129
+ return Principal(
130
+ id=user.id,
131
+ email=user.email,
132
+ username=user.username,
133
+ permissions=[
134
+ {
135
+ "resource": permission.resource,
136
+ "action": permission.action,
137
+ "scope": permission.scope,
138
+ **permission.extra,
139
+ }
140
+ for permission in permissions
141
+ ],
142
+ )
143
+
144
+
145
+ class AppPermission[T: type[Base]](BasePermission[T, int]):
146
+ auth_engine = auth_engine
147
+
148
+ async def __call__(self, principal: typing.Annotated[Principal, Depends(get_authorized_principal)]):
149
+ return await self.authorize(principal=principal)
150
+
151
+
152
+ # Note: You can use any string value as an action, enum prefered here for typed suggestions and consistency across the application.
153
+ class AuthAction(enum.StrEnum):
154
+ READ = "read"
155
+ CREATE = "create"
156
+ UPDATE = "update"
157
+ DELETE = "delete"
158
+
159
+
160
+ # 4. Define your API endpoints
161
+ @app.get("/users")
162
+ async def get_users(
163
+ auth_ctx: AuthContext = Depends(
164
+ AppPermission(
165
+ resource=User,
166
+ action=AuthAction.READ,
167
+ scopes=["global", "resource", "conditional"],
168
+ predicates=[
169
+ {
170
+ "name": "self",
171
+ "fn": lambda ctx: ctx.resource.id == ctx.principal.id,
172
+ "description": "Allow access to own user resource",
173
+ },
174
+ ],
175
+ )
176
+ ),
177
+ db: Session = Depends(get_db),
178
+ ) -> list[UserDto]:
179
+ query = select(User)
180
+ query = auth_engine.filter_query(context=auth_ctx, query=query)
181
+ users = db.execute(query).scalars().all()
182
+ return [
183
+ UserDto(id=user.id, username=user.username, email=user.email) for user in users
184
+ ]
185
+
186
+
187
+ @app.post("/users")
188
+ async def create_user(
189
+ body: UserCreateDto,
190
+ # By default, permission assumes only global scope, which is the case for most of CREATE actions
191
+ _auth: AuthContext = Depends(
192
+ AppPermission(resource=User, action=AuthAction.CREATE)
193
+ ),
194
+ db: Session = Depends(get_db),
195
+ ) -> UserDto:
196
+ user = User(username=body.username, email=body.email, password=body.password)
197
+ db.add(user)
198
+ db.commit()
199
+ db.refresh(user)
200
+ return UserDto(id=user.id, username=user.username, email=user.email)
201
+
202
+
203
+ @app.get("/articles")
204
+ async def get_articles(
205
+ auth_ctx: AuthContext = Depends(
206
+ AppPermission(
207
+ resource=Article,
208
+ action=AuthAction.READ,
209
+ scopes=["global", "resource", "conditional"],
210
+ predicates=[
211
+ {
212
+ "name": "self",
213
+ "fn": lambda ctx: ctx.resource.author_id == ctx.principal.id,
214
+ "description": "Allow access to own articles",
215
+ },
216
+ {
217
+ "name": "only_published",
218
+ "fn": lambda ctx: ctx.resource.category == "published",
219
+ "description": "Allow access to published articles only",
220
+ },
221
+ ],
222
+ )
223
+ ),
224
+ db: Session = Depends(get_db),
225
+ ) -> list[ArticleDto]:
226
+ query = select(Article)
227
+ # Note: Apply filter to query manually here. Expression will be added as AND statement to the existing filters.
228
+ query = auth_engine.filter_query(context=auth_ctx, query=query)
229
+ articles = db.execute(query).scalars().all()
230
+ return [
231
+ ArticleDto(
232
+ id=article.id,
233
+ title=article.title,
234
+ content=article.content,
235
+ author_id=article.author_id,
236
+ category=article.category,
237
+ )
238
+ for article in articles
239
+ ]
240
+
241
+ # 5. Create and store permission grants somewhere (completely up to you, check examples for to-go model definitions, auth engine only cares about Principal DTO):
242
+ grant = RolePermissionGrant(
243
+ role_id=author_role.id,
244
+ resource=Article.__resource_code__,
245
+ action=AuthAction.READ,
246
+ scope="conditional",
247
+ # Notice that we can use expression mini-DSL here to build complex conditions.
248
+ extra={"condition": "self or only_published"},
249
+ )
250
+ ```
251
+
252
+ For detailed ready-to-run examples, see [examples](examples) directory.
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "fastapi-guardian"
3
+ version = "0.1.0"
4
+ description = "Flexible permissions for FastAPI"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "achopik", email = "artikchopik@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ license = "MIT"
11
+ classifiers = [
12
+ "Operating System :: OS Independent",
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.14",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ ]
18
+ keywords = [
19
+ "fastapi", "authorization", "security"
20
+ ]
21
+ dependencies = [
22
+ "fastapi>=0.130.0",
23
+ "lark>=1.3.1",
24
+ "pydantic>=2.12.0",
25
+ ]
26
+
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/achopik/fastapi-guardian"
30
+
31
+ [project.optional-dependencies]
32
+ sqlalchemy = [
33
+ "sqlalchemy>=2.0.10"
34
+ ]
35
+ tortoise = [
36
+ "tortoise-orm>=1.1.0"
37
+ ]
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "ruff==0.15.11",
42
+ "pytest==9.0.3",
43
+ "pytest-asyncio==1.3.0",
44
+ "mypy==1.20.0",
45
+ "types-python-dateutil==2.9.0.20250516",
46
+ "pytest-cov>=7.1.0",
47
+ "factory-boy>=3.3.3",
48
+ "pytest-factoryboy>=2.8.1",
49
+ "uvicorn>=0.46.0",
50
+ "itsdangerous>=2.2.0",
51
+ ]
52
+
53
+ [build-system]
54
+ requires = ["uv_build>=0.9.28,<0.10.0"]
55
+ build-backend = "uv_build"
56
+
57
+
58
+ [tool.mypy]
59
+ python_version = "3.14"
60
+ show_error_codes = true
61
+ disable_error_code = ["misc"]
62
+ ignore_missing_imports = true
63
+ plugins = ["pydantic.mypy"]
64
+ exclude = ["alembic/versions"]
65
+ check_untyped_defs = true
@@ -0,0 +1,19 @@
1
+ from .dto import (
2
+ AuthContext,
3
+ AuthPredicate,
4
+ BasePermissionGrant,
5
+ PermissionContext,
6
+ PermissionDefinition,
7
+ PredicateContext,
8
+ Principal,
9
+ )
10
+
11
+ __all__ = [
12
+ "BasePermissionGrant",
13
+ "PermissionDefinition",
14
+ "AuthPredicate",
15
+ "PredicateContext",
16
+ "Principal",
17
+ "PermissionContext",
18
+ "AuthContext",
19
+ ]
@@ -0,0 +1,63 @@
1
+ import abc
2
+ import typing
3
+
4
+ from fastapi.exceptions import HTTPException
5
+
6
+ from fastapi_guardian import exceptions
7
+ from fastapi_guardian.dto import (
8
+ AuthContext,
9
+ AuthPredicatePayload,
10
+ AuthScope,
11
+ Identifier,
12
+ PermissionDefinition,
13
+ Principal,
14
+ )
15
+ from fastapi_guardian.engine import BaseAuthEngine
16
+ from fastapi_guardian.resource import Resource
17
+
18
+
19
+ class BasePermission[T: type[Resource], ID: Identifier](abc.ABC):
20
+ __slots__ = ("permission", "auth_engine")
21
+
22
+ auth_engine: BaseAuthEngine
23
+
24
+ def __init__(
25
+ self,
26
+ resource: T,
27
+ action: str,
28
+ scopes: list[AuthScope] | None = None,
29
+ predicates: list[AuthPredicatePayload[T, ID]] | None = None,
30
+ auth_engine: BaseAuthEngine | None = None,
31
+ ) -> None:
32
+ if auth_engine is not None:
33
+ self.auth_engine = auth_engine
34
+
35
+ if self.auth_engine is None:
36
+ raise exceptions.ImproperlyConfigured(
37
+ "auth_engine is required either as argument or as class attribute"
38
+ )
39
+
40
+ self.permission = PermissionDefinition[T, ID](
41
+ resource=resource,
42
+ action=action,
43
+ scopes=scopes or ["global"],
44
+ predicates=predicates or [],
45
+ )
46
+ self.auth_engine.register_permission(permission=self.permission)
47
+
48
+ @abc.abstractmethod
49
+ async def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> AuthContext:
50
+ pass
51
+
52
+ async def authorize(self, principal: Principal[ID] | None = None) -> AuthContext:
53
+ if principal is None:
54
+ raise HTTPException(status_code=401, detail="Unauthorized")
55
+
56
+ context = AuthContext[T, ID](
57
+ principal=principal, current_permission=self.permission
58
+ )
59
+ access_granted = self.auth_engine.has_permission(context=context)
60
+ if access_granted:
61
+ return context
62
+
63
+ raise HTTPException(status_code=403, detail="Forbidden")