fastapi-rbac-authz 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
fastapi_rbac/__init__.py CHANGED
@@ -4,13 +4,8 @@ __version__ = "0.1.0"
4
4
 
5
5
  from fastapi_rbac.context import ContextualAuthz
6
6
  from fastapi_rbac.core import RBACAuthz
7
- from fastapi_rbac.dependencies import (
8
- RBACUser,
9
- create_auth_dependency,
10
- create_authz_dependency,
11
- evaluate_permissions,
12
- )
13
- from fastapi_rbac.exceptions import Forbidden
7
+ from fastapi_rbac.dependencies import create_authz_dependency
8
+ from fastapi_rbac.errors import Forbidden
14
9
  from fastapi_rbac.permissions import (
15
10
  Contextual,
16
11
  Global,
@@ -22,14 +17,11 @@ from fastapi_rbac.router import RBACRouter
22
17
  __all__ = [
23
18
  "RBACAuthz",
24
19
  "RBACRouter",
25
- "RBACUser",
26
20
  "ContextualAuthz",
27
21
  "Global",
28
22
  "Contextual",
29
23
  "PermissionGrant",
30
24
  "PermissionScope",
31
25
  "Forbidden",
32
- "create_auth_dependency",
33
26
  "create_authz_dependency",
34
- "evaluate_permissions",
35
27
  ]
fastapi_rbac/context.py CHANGED
@@ -1,34 +1,29 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generic, TypeVar
3
2
 
4
- UserT = TypeVar("UserT")
5
3
 
6
-
7
- class ContextualAuthz(ABC, Generic[UserT]):
4
+ class ContextualAuthz(ABC):
8
5
  """Base class for contextual authorization checks.
9
6
 
10
- Subclasses are FastAPI dependencies - use standard Depends() for
11
- additional dependencies like database sessions.
7
+ Subclasses can use FastAPI dependencies in the __init__ method.
12
8
 
13
9
  Example:
14
- class MyContext(ContextualAuthz[User]):
15
- def __init__(
16
- self,
17
- user: User,
18
- request: Request,
19
- db: AsyncSession = Depends(get_db),
20
- ):
21
- self.user = user
22
- self.request = request
23
- self.db = db
24
10
 
25
- async def has_permissions(self) -> bool:
26
- # Check access using self.user, self.request, self.db
27
- return True
11
+ >>> class MyContext(ContextualAuthz):
12
+ >>> def __init__(
13
+ >>> self,
14
+ >>> user: Annotated[User, Depends(get_current_user)], # your auth dep
15
+ >>> request: Request, # fastapi dep
16
+ >>> db: AsyncSession = Depends(get_db), # your database dep
17
+ >>> ):
18
+ >>> self.user = user
19
+ >>> self.request = request
20
+ >>> self.db = db
21
+ >>>
22
+ >>> async def has_permissions(self) -> bool:
23
+ >>> # Check access using self.user, self.request, self.db
24
+ >>> return True
28
25
  """
29
26
 
30
- user: UserT
31
-
32
27
  @abstractmethod
33
28
  async def has_permissions(self) -> bool:
34
29
  """Check if the user has permission in this context.
@@ -36,4 +31,4 @@ class ContextualAuthz(ABC, Generic[UserT]):
36
31
  Returns:
37
32
  True if access should be granted, False otherwise.
38
33
  """
39
- ...
34
+ raise NotImplementedError()
fastapi_rbac/core.py CHANGED
@@ -1,42 +1,15 @@
1
1
  from collections.abc import Awaitable, Callable
2
- from typing import TYPE_CHECKING, Any, Generic, TypeVar
2
+ from typing import Any
3
3
 
4
4
  from fastapi import APIRouter, FastAPI
5
5
 
6
- from fastapi_rbac.dependencies import _rbac_user_dependency_placeholder
6
+ from fastapi_rbac.dependencies import _rbac_roles_dependency_placeholder
7
7
  from fastapi_rbac.permissions import PermissionGrant
8
+ from fastapi_rbac.router import RBACRouter
9
+ from fastapi_rbac.ui.routes import create_ui_router
8
10
 
9
- if TYPE_CHECKING:
10
- pass
11
11
 
12
- UserT = TypeVar("UserT")
13
-
14
-
15
- def _wrap_include_router(app: FastAPI, original_include_router: Callable[..., None]) -> Callable[..., None]:
16
- """Wrap FastAPI's include_router to track RBACRouters."""
17
-
18
- def wrapped_include_router(
19
- router: APIRouter,
20
- *,
21
- prefix: str = "",
22
- **kwargs: Any,
23
- ) -> None:
24
- # Import here to avoid circular import
25
- from fastapi_rbac.router import RBACRouter
26
-
27
- # Track RBAC routers in app state
28
- if isinstance(router, RBACRouter):
29
- if not hasattr(app.state, "_rbac_routers_"):
30
- app.state._rbac_routers_ = []
31
- app.state._rbac_routers_.append((prefix, router))
32
-
33
- # Call original method
34
- return original_include_router(router, prefix=prefix, **kwargs)
35
-
36
- return wrapped_include_router
37
-
38
-
39
- class RBACAuthz(Generic[UserT]):
12
+ class RBACAuthz:
40
13
  """Main RBAC authorization configuration.
41
14
 
42
15
  Attaches to a FastAPI application and provides authorization
@@ -44,68 +17,69 @@ class RBACAuthz(Generic[UserT]):
44
17
 
45
18
  Args:
46
19
  app: The FastAPI application instance.
47
- get_roles: Callable that extracts role strings from a user object.
48
20
  permissions: Mapping of role names to sets of permission grants.
49
- user_dependency: Optional FastAPI dependency that returns the authenticated user.
50
- When provided, RBAC-protected endpoints will automatically run this dependency
51
- and store the result in request.state.user before authorization checks.
21
+ roles_dependency: FastAPI dependency that returns the user's roles as set[str].
22
+ This dependency is injected into all RBAC-protected endpoints via
23
+ FastAPI's dependency_overrides mechanism, and will be evaluated before RBAC checks
52
24
  ui_path: Optional path to mount the authorization UI (e.g., "/_rbac").
53
- ui_permissions: Optional set of permissions required to access the UI.
54
25
  """
55
26
 
56
27
  def __init__(
57
28
  self,
58
29
  app: FastAPI,
59
- get_roles: Callable[[UserT], set[str]],
60
30
  permissions: dict[str, set[PermissionGrant]],
61
- user_dependency: Callable[..., UserT] | Callable[..., Awaitable[UserT]] | None = None,
31
+ roles_dependency: Callable[..., set[str]] | Callable[..., Awaitable[set[str]]],
62
32
  ui_path: str | None = None,
63
- ui_permissions: set[str] | None = None,
64
33
  ) -> None:
65
34
  self.app = app
66
- self.get_roles = get_roles
67
35
  self.permissions = permissions
68
- self.user_dependency = user_dependency
36
+ self.roles_dependency = roles_dependency
69
37
  self.ui_path = ui_path
70
- self.ui_permissions = ui_permissions
71
38
 
72
- # Initialize router tracking list
73
- if not hasattr(app.state, "_rbac_routers_"):
74
- app.state._rbac_routers_ = []
39
+ # Instance attributes for state management
40
+ self.routers: list[tuple[str, RBACRouter]] = []
41
+ self._include_router_wrapped: bool = False
75
42
 
76
43
  # Attach to app state for access from routers
77
44
  app.state.rbac = self
78
45
 
79
- # If user_dependency is provided, override the placeholder dependency
80
- # This allows the user's auth dependency to be injected into all
46
+ # Override the placeholder dependency with user's roles dependency
47
+ # This allows the roles dependency to be injected into all
81
48
  # RBAC-protected endpoints with proper FastAPI dependency resolution
82
- if user_dependency is not None:
83
- app.dependency_overrides[_rbac_user_dependency_placeholder] = user_dependency
84
-
85
- # Wrap include_router to track RBAC routers
49
+ app.dependency_overrides[_rbac_roles_dependency_placeholder] = roles_dependency
86
50
  self._wrap_app_include_router()
87
51
 
88
- # Mount UI if path specified
89
52
  if ui_path:
90
53
  self._mount_ui()
91
54
 
92
55
  def _wrap_app_include_router(self) -> None:
93
56
  """Wrap the app's include_router method to track RBACRouters."""
94
- # Only wrap if not already wrapped
95
- if hasattr(self.app, "_rbac_include_router_wrapped_"):
57
+ if self._include_router_wrapped:
96
58
  return
97
59
 
98
60
  original_include_router = self.app.include_router
99
- self.app.include_router = _wrap_include_router(self.app, original_include_router) # type: ignore[method-assign]
100
- self.app._rbac_include_router_wrapped_ = True # type: ignore[attr-defined]
61
+ self.app.include_router = self._create_wrapped_include_router(original_include_router) # type: ignore[method-assign]
62
+ self._include_router_wrapped = True
63
+
64
+ def _create_wrapped_include_router(self, original_include_router: Callable[..., None]) -> Callable[..., None]:
65
+ """Create a wrapped include_router that tracks RBACRouters."""
66
+
67
+ def wrapped_include_router(
68
+ router: APIRouter,
69
+ *,
70
+ prefix: str = "",
71
+ **kwargs: Any,
72
+ ) -> None:
73
+ if isinstance(router, RBACRouter):
74
+ self.routers.append((prefix, router))
75
+ return original_include_router(router, prefix=prefix, **kwargs)
76
+
77
+ return wrapped_include_router
101
78
 
102
79
  def _mount_ui(self) -> None:
103
80
  """Mount the authorization visualization UI."""
104
81
  if not self.ui_path:
105
82
  return
106
83
 
107
- # Import here to avoid circular import
108
- from fastapi_rbac.ui.routes import create_ui_router
109
-
110
84
  ui_router = create_ui_router(self.ui_path)
111
85
  self.app.include_router(ui_router, prefix=self.ui_path)
@@ -1,96 +1,29 @@
1
1
  import inspect
2
- from collections.abc import Awaitable, Callable, Coroutine
3
- from typing import TYPE_CHECKING, Annotated, Any, TypeVar
2
+ from collections.abc import Callable, Coroutine
3
+ from typing import Annotated, Any
4
4
 
5
5
  from fastapi import Depends, Request
6
6
 
7
7
  from fastapi_rbac.context import ContextualAuthz
8
- from fastapi_rbac.exceptions import Forbidden
8
+ from fastapi_rbac.errors import Forbidden
9
9
  from fastapi_rbac.permissions import (
10
10
  has_global_permission,
11
11
  has_permission,
12
12
  resolve_grants,
13
13
  )
14
14
 
15
- if TYPE_CHECKING:
16
- from fastapi_rbac.core import RBACAuthz
17
-
18
- UserT = TypeVar("UserT")
19
-
20
15
  # Type alias for context classes
21
- ContextClass = type[ContextualAuthz[Any]]
22
-
23
-
24
- def RBACUser(request: Request) -> Any:
25
- """Get the current authenticated user for use in context classes.
26
-
27
- This dependency reads the user from request.state.user, which is set
28
- by the auth dependency created via create_auth_dependency().
29
-
30
- Usage in context classes:
31
- class MyContext(ContextualAuthz[User]):
32
- def __init__(
33
- self,
34
- user: Annotated[User, Depends(RBACUser)],
35
- request: Request,
36
- ):
37
- self.user = user
38
- self.request = request
16
+ ContextClass = type[ContextualAuthz]
39
17
 
40
- async def has_permissions(self) -> bool:
41
- # Check permissions based on user and request context
42
- return self.user.id in allowed_users
43
18
 
44
- Returns:
45
- The authenticated user object from request.state.user.
19
+ async def _rbac_roles_dependency_placeholder(request: Request) -> set[str]: # noqa: ARG001
20
+ """Placeholder dependency for user roles.
46
21
 
47
- Raises:
48
- Forbidden: If no user is found in request state.
22
+ This placeholder is replaced at runtime via FastAPI's dependency_overrides
23
+ mechanism when RBACAuthz is initialized. The roles_dependency provided to
24
+ RBACAuthz will be used instead of this placeholder.
49
25
  """
50
- user = getattr(request.state, "user", None)
51
- if user is None:
52
- raise Forbidden("User not authenticated")
53
- return user
54
-
55
-
56
- def create_auth_dependency(
57
- rbac: "RBACAuthz[UserT]", # noqa: ARG001 - kept for API consistency with RBACRouter
58
- user_dependency: Callable[..., UserT] | Callable[..., Awaitable[UserT]],
59
- ) -> Callable[..., Coroutine[Any, Any, UserT]]:
60
- """Create a typed auth dependency for use in endpoints.
61
-
62
- Args:
63
- rbac: The RBACAuthz configuration instance. Currently unused but kept for
64
- API consistency - RBACRouter stores the rbac reference for permission
65
- evaluation.
66
- user_dependency: A FastAPI dependency that returns the authenticated user.
67
-
68
- Returns:
69
- A dependency that can be used with Depends() in endpoint signatures.
70
- """
71
-
72
- async def auth_dependency(
73
- request: Request,
74
- user: Annotated[UserT, Depends(user_dependency)],
75
- ) -> UserT:
76
- # Store user in request state for authz dependency
77
- request.state.user = user
78
- return user
79
-
80
- return auth_dependency
81
-
82
-
83
- async def _rbac_user_dependency_placeholder(request: Request) -> Any:
84
- """Placeholder dependency for user authentication.
85
-
86
- This placeholder is used in the authz_dependency signature and gets replaced
87
- at runtime via FastAPI's dependency_overrides mechanism when RBACAuthz is
88
- initialized with a user_dependency.
89
-
90
- If no user_dependency is configured, this falls back to reading from
91
- request.state.user (which must be set by the user's own auth mechanism).
92
- """
93
- return getattr(request.state, "user", None)
26
+ raise RuntimeError("RBACAuthz not configured with roles_dependency")
94
27
 
95
28
 
96
29
  def create_authz_dependency(
@@ -100,7 +33,7 @@ def create_authz_dependency(
100
33
  """Create an authorization dependency that uses FastAPI's full DI for contexts.
101
34
 
102
35
  This function creates a FastAPI dependency that:
103
- 1. Resolves user via the injected user_dependency (or placeholder fallback)
36
+ 1. Resolves a list of user roles via the injected roles_dependency
104
37
  2. Gets RBAC config from app.state.rbac
105
38
  3. Uses FastAPI's Depends(context_class) to instantiate each context
106
39
  - Context classes can use ANY FastAPI dependency patterns in __init__:
@@ -111,9 +44,9 @@ def create_authz_dependency(
111
44
  - db: Db (via Depends)
112
45
  4. Calls has_permissions() on each resolved context
113
46
 
114
- The user_dependency is injected via FastAPI's dependency_overrides mechanism.
115
- When RBACAuthz is initialized with a user_dependency, it overrides the
116
- _rbac_user_dependency_placeholder with the provided dependency.
47
+ The roles_dependency is injected via FastAPI's dependency_overrides mechanism.
48
+ When RBACAuthz is initialized with a roles_dependency, it overrides the
49
+ _rbac_roles_dependency_placeholder with the provided dependency.
117
50
 
118
51
  Args:
119
52
  required_permissions: Set of permission strings required for access.
@@ -124,10 +57,13 @@ def create_authz_dependency(
124
57
  """
125
58
  # Build context parameters - each context class becomes a Depends(context_class)
126
59
  # FastAPI will resolve all __init__ parameters automatically
60
+ if not required_permissions and not context_classes:
61
+ raise RuntimeError("Endpoint must be protected with either permissions or contexts")
62
+
127
63
  context_params: list[inspect.Parameter] = []
128
64
  for i, ctx_class in enumerate(context_classes):
129
65
  param = inspect.Parameter(
130
- f"_rbac_ctx_{i}_",
66
+ f"_fastapi_rbac_authz_ctx_{i}_",
131
67
  inspect.Parameter.KEYWORD_ONLY,
132
68
  default=None,
133
69
  annotation=Annotated[ctx_class, Depends(ctx_class)],
@@ -136,25 +72,18 @@ def create_authz_dependency(
136
72
 
137
73
  async def authz_dependency(
138
74
  request: Request,
139
- _rbac_user_: Annotated[Any, Depends(_rbac_user_dependency_placeholder)],
75
+ _rbac_roles_: Annotated[set[str], Depends(_rbac_roles_dependency_placeholder)],
140
76
  **kwargs: Any,
141
77
  ) -> None:
142
78
  """Authorization dependency that checks permissions and contexts."""
143
- # Get RBAC config from app state
144
79
  rbac = getattr(request.app.state, "rbac", None)
145
80
  if rbac is None:
146
81
  raise RuntimeError("RBACAuthz not configured. Make sure to create an RBACAuthz instance with your app.")
147
82
 
148
- # User is resolved by the injected user_dependency (or placeholder)
149
- user = _rbac_user_
150
- if user is None:
151
- raise Forbidden("User not authenticated")
152
-
153
- # Check permissions first
154
- if not required_permissions and not context_classes:
155
- raise RuntimeError("Endpoint must be protected with permissions or contexts")
83
+ roles = _rbac_roles_
84
+ if not roles:
85
+ raise Forbidden("User has no roles")
156
86
 
157
- roles = rbac.get_roles(user)
158
87
  grants = resolve_grants(roles, rbac.permissions)
159
88
 
160
89
  if not grants:
@@ -175,7 +104,7 @@ def create_authz_dependency(
175
104
  # Context instances are already resolved by FastAPI via Depends(context_class)
176
105
  if need_contextual_check:
177
106
  for i in range(len(context_classes)):
178
- context = kwargs.get(f"_rbac_ctx_{i}_")
107
+ context = kwargs.get(f"_fastapi_rbac_authz_ctx_{i}_")
179
108
  if context is not None and not await context.has_permissions():
180
109
  raise Forbidden()
181
110
 
@@ -187,68 +116,13 @@ def create_authz_dependency(
187
116
  annotation=Request,
188
117
  ),
189
118
  inspect.Parameter(
190
- "_rbac_user_",
119
+ "_rbac_roles_",
191
120
  inspect.Parameter.KEYWORD_ONLY,
192
121
  default=None,
193
- annotation=Annotated[Any, Depends(_rbac_user_dependency_placeholder)],
122
+ annotation=Annotated[set[str], Depends(_rbac_roles_dependency_placeholder)],
194
123
  ),
195
124
  ]
196
125
 
197
126
  new_sig = inspect.Signature(parameters=base_params + context_params)
198
127
  authz_dependency.__signature__ = new_sig # type: ignore[attr-defined]
199
-
200
128
  return authz_dependency
201
-
202
-
203
- async def evaluate_permissions(
204
- user: Any,
205
- request: Request,
206
- rbac: "RBACAuthz[Any]",
207
- required_permissions: set[str],
208
- context_classes: list[ContextClass],
209
- ) -> None:
210
- """Directly evaluate permissions without FastAPI dependency injection.
211
-
212
- This function is useful for testing or when you need to check permissions
213
- outside of a FastAPI endpoint context. Unlike create_authz_dependency(),
214
- this function instantiates context classes directly with user and request.
215
-
216
- Args:
217
- user: The authenticated user object.
218
- request: The current HTTP request.
219
- rbac: The RBACAuthz configuration instance.
220
- required_permissions: Set of permission strings required for access.
221
- context_classes: List of ContextualAuthz subclasses to check.
222
-
223
- Raises:
224
- Forbidden: If the user does not have the required permissions.
225
- RuntimeError: If no permissions or contexts are specified.
226
- """
227
- if not required_permissions and not context_classes:
228
- raise RuntimeError("Endpoint must be protected with permissions or contexts")
229
-
230
- roles = rbac.get_roles(user)
231
- grants = resolve_grants(roles, rbac.permissions)
232
-
233
- if not grants:
234
- raise Forbidden()
235
-
236
- need_contextual_check = not required_permissions
237
-
238
- for required in required_permissions:
239
- if has_global_permission(grants, required):
240
- # Global permission - no need for contextual check for this permission
241
- continue
242
-
243
- need_contextual_check = True
244
- if not has_permission(grants, required):
245
- raise Forbidden()
246
-
247
- # Run contextual checks if needed
248
- # Instantiate context classes directly with user and request
249
- if need_contextual_check:
250
- for ctx_class in context_classes:
251
- # Context classes are expected to accept user and request in their __init__
252
- context = ctx_class(user=user, request=request) # type: ignore[call-arg]
253
- if not await context.has_permissions():
254
- raise Forbidden()
@@ -1,8 +1,7 @@
1
1
  from fastapi import HTTPException
2
+ from starlette.status import HTTP_403_FORBIDDEN
2
3
 
3
4
 
4
5
  class Forbidden(HTTPException):
5
- """403 Forbidden - user lacks required permissions."""
6
-
7
6
  def __init__(self, detail: str = "Forbidden") -> None:
8
- super().__init__(status_code=403, detail=detail)
7
+ super().__init__(status_code=HTTP_403_FORBIDDEN, detail=detail)
@@ -23,9 +23,11 @@ class PermissionGrant:
23
23
  def __hash__(self) -> int:
24
24
  return hash((self.permission, self.scope))
25
25
 
26
- def __repr__(self) -> str:
26
+ def __str__(self) -> str:
27
27
  return f"{self.__class__.__name__}({self.permission!r})"
28
28
 
29
+ __repr__ = __str__
30
+
29
31
 
30
32
  class Global(PermissionGrant):
31
33
  """A global permission grant - bypasses contextual checks."""
fastapi_rbac/router.py CHANGED
@@ -9,35 +9,30 @@ from fastapi import APIRouter, Depends
9
9
 
10
10
  from fastapi_rbac.context import ContextualAuthz
11
11
  from fastapi_rbac.dependencies import create_authz_dependency
12
- from fastapi_rbac.permissions import WILDCARD
12
+ from fastapi_rbac.permissions import SEPARATOR, WILDCARD
13
13
 
14
- # Type alias for context classes
15
- ContextClass = type[ContextualAuthz[Any]]
14
+ ContextClass = type[ContextualAuthz] # alias
16
15
 
17
16
 
18
- def _contains_wildcard(permission: str) -> bool:
19
- """Check if a permission contains a wildcard."""
20
- return WILDCARD in permission
21
-
22
-
23
- def _validate_permissions(permissions: set[str] | None, location: str) -> None:
24
- """Validate that permissions don't contain wildcards.
25
-
26
- Args:
27
- permissions: Set of permission strings to validate.
28
- location: Description of where the permissions are defined (for error message).
29
-
30
- Raises:
31
- RuntimeError: If any permission contains a wildcard.
32
- """
17
+ def _validate_permissions(
18
+ permissions: set[str] | None,
19
+ location: str,
20
+ ) -> None:
21
+ """Various startup permissions validations"""
33
22
  if permissions is None:
34
23
  return
35
24
  for perm in permissions:
36
- if _contains_wildcard(perm):
25
+ if WILDCARD in perm:
37
26
  raise RuntimeError(
38
27
  f"Wildcard permissions are not allowed in {location}. "
39
28
  f"Found '{perm}'. Wildcards should only be used in role grants."
40
29
  )
30
+ if SEPARATOR not in perm:
31
+ raise RuntimeError(
32
+ f"Each permission must decline at least one resource and one action. Found '{perm}' at {location}"
33
+ )
34
+ if perm.startswith(SEPARATOR) or perm.startswith(WILDCARD):
35
+ raise RuntimeError(f"Permission must explicitly define resource. Found '{perm}' at {location}")
41
36
 
42
37
 
43
38
  class RBACRouter(APIRouter):
@@ -73,7 +68,6 @@ class RBACRouter(APIRouter):
73
68
  contexts: list[ContextClass] | None = None,
74
69
  **kwargs: Any,
75
70
  ) -> None:
76
- # Validate no wildcards in router-level permissions
77
71
  _validate_permissions(permissions, "router permissions")
78
72
 
79
73
  super().__init__(**kwargs)
@@ -81,24 +75,6 @@ class RBACRouter(APIRouter):
81
75
  self.default_contexts: list[ContextClass] = contexts or []
82
76
  self.endpoint_metadata: dict[tuple[str, str], dict[str, Any]] = {}
83
77
 
84
- def _create_authz_dependency(
85
- self,
86
- permissions: set[str],
87
- contexts: list[ContextClass],
88
- ) -> Callable[..., Any]:
89
- """Create an authorization dependency for an endpoint.
90
-
91
- This dependency will be injected into the endpoint and will check
92
- permissions before the endpoint handler is called.
93
-
94
- Uses create_authz_dependency from dependencies.py which supports
95
- resolving Depends() parameters in context classes.
96
- """
97
- return create_authz_dependency(
98
- required_permissions=permissions,
99
- context_classes=contexts,
100
- )
101
-
102
78
  def _resolve_permissions_and_contexts(
103
79
  self,
104
80
  path: str,
@@ -117,13 +93,8 @@ class RBACRouter(APIRouter):
117
93
  Returns:
118
94
  Tuple of (final_permissions, final_contexts).
119
95
  """
120
- # Validate no wildcards in endpoint-level permissions
121
96
  _validate_permissions(permissions, "endpoint permissions")
122
-
123
- # Resolve final permissions: endpoint overrides router
124
97
  final_permissions = permissions if permissions is not None else self.default_permissions
125
-
126
- # Resolve final contexts: endpoint merges with router
127
98
  final_contexts = list(self.default_contexts)
128
99
  if contexts:
129
100
  final_contexts.extend(contexts)
@@ -162,33 +133,16 @@ class RBACRouter(APIRouter):
162
133
  new_params = params + [authz_param]
163
134
  new_sig = sig.replace(parameters=new_params)
164
135
 
165
- # Determine if endpoint is async
166
- is_async = inspect.iscoroutinefunction(endpoint)
167
-
168
- if is_async:
169
-
170
- @wraps(endpoint)
171
- async def wrapped_async(
172
- *args: Any,
173
- _rbac_authz_check_: Annotated[None, Depends(authz_dep)] = None,
174
- **kwargs: Any,
175
- ) -> Any:
176
- return await endpoint(*args, **kwargs)
177
-
178
- wrapped_async.__signature__ = new_sig # type: ignore[attr-defined]
179
- return wrapped_async
180
- else:
181
-
182
- @wraps(endpoint)
183
- def wrapped_sync(
184
- *args: Any,
185
- _rbac_authz_check_: Annotated[None, Depends(authz_dep)] = None,
186
- **kwargs: Any,
187
- ) -> Any:
188
- return endpoint(*args, **kwargs)
136
+ @wraps(endpoint)
137
+ async def wrapped_async(
138
+ *args: Any,
139
+ _rbac_authz_check_: Annotated[None, Depends(authz_dep)] = None,
140
+ **kwargs: Any,
141
+ ) -> Any:
142
+ return await endpoint(*args, **kwargs)
189
143
 
190
- wrapped_sync.__signature__ = new_sig # type: ignore[attr-defined]
191
- return wrapped_sync
144
+ wrapped_async.__signature__ = new_sig # type: ignore[attr-defined]
145
+ return wrapped_async
192
146
 
193
147
  def _add_route_with_authz(
194
148
  self,
@@ -218,7 +172,7 @@ class RBACRouter(APIRouter):
218
172
 
219
173
  # If there are permissions or contexts, wrap the endpoint
220
174
  if final_permissions or final_contexts:
221
- authz_dep = self._create_authz_dependency(final_permissions, final_contexts)
175
+ authz_dep = create_authz_dependency(final_permissions, final_contexts)
222
176
  endpoint = self._wrap_endpoint_with_authz(endpoint, authz_dep)
223
177
 
224
178
  # Attach RBAC metadata to endpoint for later introspection
fastapi_rbac/ui/routes.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING
7
7
 
8
8
  from fastapi import APIRouter, Request
9
9
  from fastapi.responses import HTMLResponse, JSONResponse
@@ -59,8 +59,7 @@ def create_ui_router(ui_path: str) -> APIRouter:
59
59
  include_in_schema=False,
60
60
  )
61
61
  async def get_schema(request: Request) -> JSONResponse:
62
- """Return the RBAC schema as JSON."""
63
- rbac: RBACAuthz[Any] = request.app.state.rbac
62
+ rbac: RBACAuthz = request.app.state.rbac
64
63
  schema = build_ui_schema(request.app, rbac)
65
64
  return JSONResponse(content=schema.model_dump())
66
65
 
fastapi_rbac/ui/schema.py CHANGED
@@ -147,8 +147,8 @@ def _build_endpoints_schema(app: FastAPI) -> tuple[list[EndpointSchema], dict[st
147
147
  context_classes: dict[str, type] = {}
148
148
  seen_endpoints: set[tuple[str, str]] = set()
149
149
 
150
- # Get all registered RBAC routers from app state
151
- rbac_routers: list[tuple[str, Any]] = getattr(app.state, "_rbac_routers_", [])
150
+ # Get all registered RBAC routers from RBACAuthz instance
151
+ rbac_routers: list[tuple[str, Any]] = getattr(app.state.rbac, "routers", [])
152
152
 
153
153
  # Build metadata map from all registered routers
154
154
  metadata_map: dict[tuple[str, str], dict[str, Any]] = {}
@@ -232,7 +232,7 @@ def _build_contexts_schema(
232
232
  ]
233
233
 
234
234
 
235
- def build_ui_schema(app: FastAPI, rbac: RBACAuthz[Any]) -> UISchema:
235
+ def build_ui_schema(app: FastAPI, rbac: RBACAuthz) -> UISchema:
236
236
  """Build the complete UI schema for RBAC visualization.
237
237
 
238
238
  Args:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-rbac-authz
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Role-based access control with contextual authorization for FastAPI
5
5
  Project-URL: Homepage, https://github.com/parikls/fastapi-rbac
6
6
  Author-email: Dmytro Smyk <porovozls@gmail.com>
@@ -36,7 +36,7 @@ pip install fastapi-rbac-authz
36
36
  from typing import Annotated
37
37
  from fastapi import Depends, FastAPI
38
38
  from fastapi_rbac import (
39
- RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz, RBACUser
39
+ RBACAuthz, RBACRouter, Global, Contextual, ContextualAuthz
40
40
  )
41
41
 
42
42
  # 1. Define your user model
@@ -45,7 +45,17 @@ class User:
45
45
  self.user_id = user_id
46
46
  self.roles = roles
47
47
 
48
- # 2. Define role permissions
48
+ # 2. Define your authentication dependencies
49
+ # The library only needs roles - you can authenticate however you like
50
+ async def get_current_user() -> User:
51
+ # Your authentication logic here
52
+ return User(user_id="user-1", roles={"viewer"})
53
+
54
+ async def get_current_user_roles() -> set[str]:
55
+ user = await get_current_user()
56
+ return user.roles
57
+
58
+ # 3. Define role permissions
49
59
  PERMISSIONS = {
50
60
  "admin": {
51
61
  Global("report:*"), # Admin can do anything with reports
@@ -55,13 +65,13 @@ PERMISSIONS = {
55
65
  },
56
66
  }
57
67
 
58
- # 3. Create a context check (for contextual permissions)
59
- # All __init__ params are injected by FastAPI's DI system
60
- class ReportAccessContext(ContextualAuthz[User]):
68
+ # 4. Create a context check (for contextual permissions)
69
+ # Context classes are responsible for their own authentication via FastAPI DI
70
+ class ReportAccessContext(ContextualAuthz):
61
71
  def __init__(
62
72
  self,
63
73
  report_id: int, # <-- Injected from path parameter
64
- user: Annotated[User, Depends(RBACUser)],
74
+ user: Annotated[User, Depends(get_current_user)], # Your auth dependency
65
75
  ):
66
76
  self.user = user
67
77
  self.report_id = report_id
@@ -71,22 +81,17 @@ class ReportAccessContext(ContextualAuthz[User]):
71
81
  allowed_reports = {1, 2, 3} # e.g., query from database
72
82
  return self.report_id in allowed_reports
73
83
 
74
- # 4. Create your app and configure RBAC
84
+ # 5. Create your app and configure RBAC
75
85
  app = FastAPI()
76
86
 
77
- async def get_current_user() -> User:
78
- # Your authentication logic here
79
- return User(user_id="user-1", roles={"viewer"})
80
-
81
87
  RBACAuthz(
82
88
  app,
83
- get_roles=lambda u: u.roles,
84
89
  permissions=PERMISSIONS,
85
- user_dependency=get_current_user,
90
+ roles_dependency=get_current_user_roles, # Returns set[str]
86
91
  ui_path="/_rbac", # Optional: mount visualization UI
87
92
  )
88
93
 
89
- # 5. Create protected routes
94
+ # 6. Create protected routes
90
95
  router = RBACRouter(permissions={"report:read"}, contexts=[ReportAccessContext])
91
96
 
92
97
  @router.get("/reports/{report_id}")
@@ -100,6 +105,8 @@ async def create_report():
100
105
  app.include_router(router, prefix="/api")
101
106
  ```
102
107
 
108
+ > **Note:** The library doesn't care how you authenticate - whether via dependency, middleware, JWT decode, or any other method. You just need to provide a dependency that returns the user's roles as `set[str]`. Context classes are responsible for their own authentication and can use any FastAPI dependency pattern.
109
+
103
110
  ## Permission Scopes
104
111
 
105
112
  Permissions can be granted with two scopes:
@@ -136,14 +143,14 @@ PERMISSIONS = {
136
143
 
137
144
  ## Context Checks
138
145
 
139
- Context checks are classes that implement fine-grained authorization logic. They're regular FastAPI dependencies, so you can inject any parameters (path params, query params, request body, database sessions, etc.).
146
+ Context checks are classes that implement fine-grained authorization logic. They're regular FastAPI dependencies, so you can inject any parameters (path params, query params, request body, database sessions, etc.). Each context class is responsible for its own authentication via FastAPI's dependency injection.
140
147
 
141
148
  ```python
142
- class ReportAccessContext(ContextualAuthz[User]):
149
+ class ReportAccessContext(ContextualAuthz):
143
150
  def __init__(
144
151
  self,
145
152
  report_id: int, # Injected from path parameter
146
- user: Annotated[User, Depends(RBACUser)],
153
+ user: Annotated[User, Depends(get_current_user)], # Your auth dependency
147
154
  db: Annotated[AsyncSession, Depends(get_db)], # Database session
148
155
  ):
149
156
  self.user = user
@@ -161,8 +168,8 @@ class ReportAccessContext(ContextualAuthz[User]):
161
168
  When a request hits an RBAC-protected endpoint:
162
169
 
163
170
  ```
164
- 1. Authentication
165
- └── user_dependency runs → User object available
171
+ 1. Role Resolution
172
+ └── roles_dependency runs → User's roles (set[str]) available
166
173
 
167
174
  2. Permission Check
168
175
  └── Does user have ANY grant (global or contextual) for required permission?
@@ -175,7 +182,7 @@ When a request hits an RBAC-protected endpoint:
175
182
  └── No → Continue to context checks
176
183
 
177
184
  4. Context Checks (only for Contextual grants)
178
- └── Run all context classes via FastAPI DI
185
+ └── Run all context classes via FastAPI DI (each gets its own user via Depends)
179
186
  └── Do ALL contexts return True?
180
187
  ├── No → 403 Forbidden
181
188
  └── Yes → Access granted
@@ -0,0 +1,15 @@
1
+ fastapi_rbac/__init__.py,sha256=CqpNJ54-bMbUF-LrQUAGX9pfTBYXR6x13kG1EDm5odY,662
2
+ fastapi_rbac/context.py,sha256=7lFQotJU4Wn-_BcHi3A8bAB7ftzVKJRJoXzs3q-I4tY,1039
3
+ fastapi_rbac/core.py,sha256=pY8UjpSxf1xV5ODp2b15_fU16sUfxv32_5vipQIX4XU,3246
4
+ fastapi_rbac/dependencies.py,sha256=zukBpbvB9586Lz_df7t1F-CA-cnhDy6R4aJo1FlSpr4,4916
5
+ fastapi_rbac/errors.py,sha256=Df2zWPe-Kd54bKTB4w40wUTuNnppfCkZ33cJqnylIgo,247
6
+ fastapi_rbac/permissions.py,sha256=lU1K2rHYqkxjYrnkG1Rk2FKEmgw0py62bNkOgSmznA8,2728
7
+ fastapi_rbac/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
8
+ fastapi_rbac/router.py,sha256=4BeKXAEqcdFh4kX9AJaUuCmpVXJXU07Afubhb20Z7B8,9943
9
+ fastapi_rbac/ui/__init__.py,sha256=u2enI_c9k1d0T0z1kBuGI4lpR0wifqFzkfaEUEnuLXg,220
10
+ fastapi_rbac/ui/routes.py,sha256=UbJcMo6mMNNtzlSAdrp1S79wppkO4hFC018POzlsq-k,2025
11
+ fastapi_rbac/ui/schema.py,sha256=Q7diGVuNuGHEGn_uq7OH5Cx2dE9QHfAEdLt70_BzpYE,7919
12
+ fastapi_rbac/ui/static/index.html,sha256=HUxI7eO84duCxxOi9GqP4yybA6QrxQAbNcc6lYkjM30,73455
13
+ fastapi_rbac_authz-0.4.0.dist-info/METADATA,sha256=2H4g3gBXal5phFSnAdgpIfhtoyNgeoILke4yAHqMVFs,8730
14
+ fastapi_rbac_authz-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ fastapi_rbac_authz-0.4.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- fastapi_rbac/__init__.py,sha256=pmId7xpntLKSwaQaFmBwJa443-FyfGOFwl_pf8t04sE,817
2
- fastapi_rbac/context.py,sha256=txZqnw8xGPHsE3DQi75Q7ny_VifT0O2etcz5E8sm2wE,1070
3
- fastapi_rbac/core.py,sha256=smTSbxCQYyMBYvkzOavPurIMWKXrp1ZHWXOs7WVwVvY,4154
4
- fastapi_rbac/dependencies.py,sha256=2475jW8QPRUWubA__M48D-EY8GbL2SJH8mLaaEMoLfg,9325
5
- fastapi_rbac/exceptions.py,sha256=tSFbes6yzAygWXXl9NLdZ5K2_iISiJ6ZGJCD6077i94,244
6
- fastapi_rbac/permissions.py,sha256=VReUjewdFehkDpA_rowSAV2qFoAfoDhrLvoU2gGSxww,2705
7
- fastapi_rbac/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
8
- fastapi_rbac/router.py,sha256=kLUmWmBZKqNoGvoD44g-Un9tCIO_dP3psQYrNBm5QFI,11405
9
- fastapi_rbac/ui/__init__.py,sha256=u2enI_c9k1d0T0z1kBuGI4lpR0wifqFzkfaEUEnuLXg,220
10
- fastapi_rbac/ui/routes.py,sha256=d57_bPsrpFePzumqcHIU3Okf8TfXKkqf8KLhkrvCg_I,2081
11
- fastapi_rbac/ui/schema.py,sha256=p3mWgkpA20jYI_g7KiIdULXCFzSeNXBMIL9c7Pe7Zgg,7917
12
- fastapi_rbac/ui/static/index.html,sha256=HUxI7eO84duCxxOi9GqP4yybA6QrxQAbNcc6lYkjM30,73455
13
- fastapi_rbac_authz-0.2.0.dist-info/METADATA,sha256=EPHDOLeaGOgDBFWyDzEpgowV1DlFgx3HhJXVP5YBSTI,7985
14
- fastapi_rbac_authz-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- fastapi_rbac_authz-0.2.0.dist-info/RECORD,,