permissions2fast-fastapi 0.1.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.
Files changed (36) hide show
  1. permissions2fast_fastapi/__init__.py +26 -0
  2. permissions2fast_fastapi/__version__.py +1 -0
  3. permissions2fast_fastapi/dependencies.py +123 -0
  4. permissions2fast_fastapi/models/__init__.py +21 -0
  5. permissions2fast_fastapi/models/permission_assignment_model.py +13 -0
  6. permissions2fast_fastapi/models/permission_category_model.py +18 -0
  7. permissions2fast_fastapi/models/permission_model.py +17 -0
  8. permissions2fast_fastapi/models/permission_route_model.py +13 -0
  9. permissions2fast_fastapi/models/role_model.py +21 -0
  10. permissions2fast_fastapi/models/route_model.py +14 -0
  11. permissions2fast_fastapi/models/tenant_model.py +16 -0
  12. permissions2fast_fastapi/models/user_role_model.py +12 -0
  13. permissions2fast_fastapi/models/user_tenant_role_model.py +15 -0
  14. permissions2fast_fastapi/routers/__init__.py +5 -0
  15. permissions2fast_fastapi/routers/permissions_router.py +191 -0
  16. permissions2fast_fastapi/routers/roles_router.py +177 -0
  17. permissions2fast_fastapi/routers/routes_router.py +44 -0
  18. permissions2fast_fastapi/schemas/__init__.py +53 -0
  19. permissions2fast_fastapi/schemas/permission_category_schema.py +10 -0
  20. permissions2fast_fastapi/schemas/permission_schema.py +35 -0
  21. permissions2fast_fastapi/schemas/role_schema.py +40 -0
  22. permissions2fast_fastapi/schemas/route_schema.py +11 -0
  23. permissions2fast_fastapi/services/__init__.py +6 -0
  24. permissions2fast_fastapi/services/access_service.py +110 -0
  25. permissions2fast_fastapi/services/permission_service.py +197 -0
  26. permissions2fast_fastapi/services/role_service.py +256 -0
  27. permissions2fast_fastapi/services/route_service.py +44 -0
  28. permissions2fast_fastapi/settings.py +64 -0
  29. permissions2fast_fastapi/utils/__init__.py +27 -0
  30. permissions2fast_fastapi/utils/permission_cache.py +251 -0
  31. permissions2fast_fastapi/utils/redis_client.py +82 -0
  32. permissions2fast_fastapi-0.1.0.dist-info/METADATA +141 -0
  33. permissions2fast_fastapi-0.1.0.dist-info/RECORD +36 -0
  34. permissions2fast_fastapi-0.1.0.dist-info/WHEEL +5 -0
  35. permissions2fast_fastapi-0.1.0.dist-info/licenses/LICENSE +21 -0
  36. permissions2fast_fastapi-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,26 @@
1
+ """
2
+ permissions2fast-fastapi
3
+
4
+ Complete RBAC (Role-Based Access Control) system for FastAPI applications.
5
+ Provides role management, permission checking, and user-role assignments.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ from .models.role_model import Role
11
+ from .models.permission_model import Permission
12
+ from .models.permission_category_model import PermissionCategory
13
+ from .models.route_model import Route
14
+ from .models.user_role_model import UserRole
15
+ from .models.permission_assignment_model import PermissionAssignment
16
+ from .models.permission_route_model import PermissionRoute
17
+
18
+ __all__ = [
19
+ "Role",
20
+ "Permission",
21
+ "PermissionCategory",
22
+ "Route",
23
+ "UserRole",
24
+ "PermissionAssignment",
25
+ "PermissionRoute",
26
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,123 @@
1
+ """
2
+ FastAPI Dependencies
3
+
4
+ Dependencies for protecting routes based on roles and permissions.
5
+ """
6
+
7
+ from typing import Annotated
8
+
9
+ from fastapi import Depends, HTTPException, Request, status
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ # Import from oauth2fast-fastapi directly
13
+ from oauth2fast_fastapi.dependencies import get_current_verified_user, get_auth_session
14
+ from oauth2fast_fastapi import User
15
+
16
+ from .services import access_service, role_service
17
+
18
+
19
+ def has_role(
20
+ role_name: str,
21
+ ):
22
+ """
23
+ Dependency to require a specific role.
24
+
25
+ Usage:
26
+ @app.get("/admin")
27
+ def admin_route(user: User = Depends(has_role("admin"))):
28
+ ...
29
+ """
30
+ async def _has_role(
31
+ request: Request,
32
+ user: Annotated[User, Depends(get_current_verified_user)],
33
+ session: Annotated[AsyncSession, Depends(get_auth_session)],
34
+ ) -> User:
35
+ tenant_id = get_tenant_id(request)
36
+ user_roles = await role_service.list_user_roles(user.id, session, tenant_id=tenant_id)
37
+
38
+ has_required = any(r.name == role_name for r in user_roles)
39
+
40
+ if not has_required:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_403_FORBIDDEN,
43
+ detail=f"Missing required role: {role_name}",
44
+ )
45
+
46
+ return user
47
+
48
+ return _has_role
49
+
50
+
51
+ def get_tenant_id(request: Request) -> int | None:
52
+ """
53
+ Extract tenant_id from request.
54
+ Priority:
55
+ 1. request.state.tenant_id (injected by external middleware)
56
+ 2. X-Tenant-ID header
57
+ """
58
+ from .settings import settings
59
+ if not settings.enable_tenancy:
60
+ return None
61
+
62
+ # First check state (injected by middleware)
63
+ if hasattr(request.state, "tenant_id") and request.state.tenant_id:
64
+ try:
65
+ return int(request.state.tenant_id)
66
+ except ValueError:
67
+ pass
68
+
69
+ # Fallback to header
70
+ header_tenant = request.headers.get("X-Tenant-ID")
71
+ if header_tenant:
72
+ try:
73
+ return int(header_tenant)
74
+ except ValueError:
75
+ pass
76
+
77
+ return None
78
+
79
+
80
+ def has_permission(
81
+ permission_route: str | None = None,
82
+ method: str | None = None,
83
+ ):
84
+ """
85
+ Dependency to require permission for a route.
86
+
87
+ If params are None, verifies access to the *current* request path and method.
88
+
89
+ Usage:
90
+ @app.get("/items")
91
+ def items(user: User = Depends(has_permission())):
92
+ # Checks if user can GET /items
93
+ ...
94
+
95
+ @app.get("/items")
96
+ def items(user: User = Depends(has_permission(permission_route="/api/items", method="GET"))):
97
+ ...
98
+ """
99
+ async def _has_permission(
100
+ request: Request,
101
+ user: Annotated[User, Depends(get_current_verified_user)],
102
+ session: Annotated[AsyncSession, Depends(get_auth_session)],
103
+ ) -> User:
104
+ # Determine what to check
105
+ route_to_check = permission_route or request.url.path
106
+ method_to_check = method or request.method
107
+
108
+ # Extract tenant context
109
+ tenant_id = get_tenant_id(request)
110
+
111
+ is_allowed = await access_service.check_user_access(
112
+ user.id, route_to_check, method_to_check, session, tenant_id=tenant_id
113
+ )
114
+
115
+ if not is_allowed:
116
+ raise HTTPException(
117
+ status_code=status.HTTP_403_FORBIDDEN,
118
+ detail="Not enough permissions to access this resource",
119
+ )
120
+
121
+ return user
122
+
123
+ return _has_permission
@@ -0,0 +1,21 @@
1
+ from .role_model import Role
2
+ from .permission_model import Permission
3
+ from .permission_category_model import PermissionCategory
4
+ from .route_model import Route
5
+ from .user_role_model import UserRole
6
+ from .permission_assignment_model import PermissionAssignment
7
+ from .permission_route_model import PermissionRoute
8
+ from .tenant_model import Tenant
9
+ from .user_tenant_role_model import UserTenantRole
10
+
11
+ __all__ = [
12
+ "Role",
13
+ "Permission",
14
+ "PermissionCategory",
15
+ "Route",
16
+ "UserRole",
17
+ "PermissionAssignment",
18
+ "PermissionRoute",
19
+ "Tenant",
20
+ "UserTenantRole",
21
+ ]
@@ -0,0 +1,13 @@
1
+ from sqlmodel import Field, SQLModel
2
+ from oauth2fast_fastapi.models import AuthModel
3
+
4
+ class PermissionAssignment(AuthModel, table=True):
5
+ """
6
+ Polymorphic Permission Assignment for RBAC.
7
+ Description: Assigns a permission directly to an entity (e.g. User or Role).
8
+ """
9
+ __tablename__ = "permission_assignments"
10
+
11
+ permission_id: int = Field(primary_key=True, foreign_key="permissions.id")
12
+ entity_type: str = Field(primary_key=True, index=True) # e.g., "User", "Team"
13
+ entity_id: int = Field(primary_key=True, index=True)
@@ -0,0 +1,18 @@
1
+ from sqlmodel import Field, SQLModel, Relationship
2
+ from typing import Optional, List
3
+
4
+ # Forward reference for relationship
5
+ from oauth2fast_fastapi.models import AuthModel
6
+
7
+ class PermissionCategory(AuthModel, table=True):
8
+ """
9
+ Permission Category definition for RBAC.
10
+ Description: Groups related permissions together.
11
+ """
12
+ __tablename__ = "permission_categories"
13
+
14
+ id: Optional[int] = Field(default=None, primary_key=True)
15
+ name: str = Field(index=True, unique=True)
16
+
17
+ # Relationships
18
+ # permissions: List["Permission"] = Relationship(back_populates="category")
@@ -0,0 +1,17 @@
1
+ from sqlmodel import Field, SQLModel, Relationship
2
+ from typing import Optional
3
+ from oauth2fast_fastapi.models import AuthModel
4
+
5
+ class Permission(AuthModel, table=True):
6
+ """
7
+ Permission definition for RBAC.
8
+ Description: Represents a specific action or access right.
9
+ """
10
+ __tablename__ = "permissions"
11
+
12
+ id: Optional[int] = Field(default=None, primary_key=True)
13
+ name: str = Field(index=True, unique=True)
14
+ permission_category_id: int = Field(foreign_key="permission_categories.id")
15
+
16
+ # Relationships
17
+ # category: "PermissionCategory" = Relationship(back_populates="permissions")
@@ -0,0 +1,13 @@
1
+ from sqlmodel import Field, SQLModel
2
+ from oauth2fast_fastapi.models import AuthModel
3
+
4
+ class PermissionRoute(AuthModel, table=True):
5
+ """
6
+ Permission Route mapping for RBAC.
7
+ Description: Maps permissions to specific API routes.
8
+ """
9
+ __tablename__ = "permission_routes"
10
+
11
+ id: int | None = Field(default=None, primary_key=True)
12
+ permission_id: int = Field(foreign_key="permissions.id")
13
+ route_id: int = Field(foreign_key="routes.id")
@@ -0,0 +1,21 @@
1
+ from sqlmodel import BigInteger, Column, Field
2
+ from sqlmodel import BigInteger, Column, Field, SQLModel
3
+
4
+ from oauth2fast_fastapi.models import AuthModel
5
+
6
+
7
+ class Role(AuthModel, table=True):
8
+ """
9
+ Role definition for RBAC.
10
+ Description: Role model
11
+ """
12
+
13
+ __tablename__ = "roles"
14
+
15
+ id: int = Field(
16
+ default=None, sa_column=Column(BigInteger, index=True, primary_key=True)
17
+ )
18
+ name: str = Field(index=True, unique=True)
19
+ description: str | None = Field(default=None)
20
+ is_active: bool = Field(default=True)
21
+
@@ -0,0 +1,14 @@
1
+ from sqlmodel import Field, SQLModel
2
+ from typing import Optional
3
+ from oauth2fast_fastapi.models import AuthModel
4
+
5
+ class Route(AuthModel, table=True):
6
+ """
7
+ Route definition for RBAC.
8
+ Description: Represents an API endpoint or accessible resource path.
9
+ """
10
+ __tablename__ = "routes"
11
+
12
+ id: Optional[int] = Field(default=None, primary_key=True)
13
+ name: str = Field(index=True, unique=True) # The path
14
+ is_active: bool = Field(default=True)
@@ -0,0 +1,16 @@
1
+ from sqlmodel import Field, SQLModel
2
+ from oauth2fast_fastapi.models import AuthModel
3
+
4
+
5
+ class Tenant(AuthModel, table=True):
6
+ """
7
+ Tenant definition for Multi-tenant RBAC.
8
+ Description: Represents an isolated organization or tenant.
9
+ """
10
+ __tablename__ = "tenants"
11
+
12
+ id: int | None = Field(default=None, primary_key=True)
13
+ name: str = Field(index=True)
14
+ schema_name: str | None = Field(default=None, description="DB Schema name if applicable")
15
+ db_url: str | None = Field(default=None, description="External DB URL if applicable")
16
+ is_active: bool = Field(default=True)
@@ -0,0 +1,12 @@
1
+ from sqlmodel import Field, SQLModel
2
+ from oauth2fast_fastapi.models import AuthModel
3
+
4
+ class UserRole(AuthModel, table=True):
5
+ """
6
+ User Role mapping for RBAC.
7
+ Description: Maps users to their assigned roles.
8
+ """
9
+ __tablename__ = "user_role"
10
+
11
+ role_id: int = Field(primary_key=True, foreign_key="roles.id")
12
+ user_id: int = Field(primary_key=True, index=True, foreign_key="users.id")
@@ -0,0 +1,15 @@
1
+ from sqlmodel import Field, SQLModel
2
+ from oauth2fast_fastapi.models import AuthModel
3
+
4
+
5
+ class UserTenantRole(AuthModel, table=True):
6
+ """
7
+ Tenant-specific Role mapping for RBAC.
8
+ Description: Maps users to roles within a specific tenant context.
9
+ """
10
+ __tablename__ = "user_tenant_roles"
11
+
12
+ id: int | None = Field(default=None, primary_key=True)
13
+ user_id: int = Field(index=True, foreign_key="users.id")
14
+ tenant_id: int = Field(foreign_key="tenants.id", index=True)
15
+ role_id: int = Field(foreign_key="roles.id", index=True)
@@ -0,0 +1,5 @@
1
+ from .permissions_router import router as permissions_router
2
+ from .roles_router import router as roles_router
3
+ from .routes_router import router as routes_router
4
+
5
+ __all__ = ["permissions_router", "roles_router", "routes_router"]
@@ -0,0 +1,191 @@
1
+ """
2
+ Permission Management Router
3
+
4
+ Endpoints for managing permissions, categories, and assignments.
5
+ """
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from ..schemas.permission_schema import (
11
+ PermissionCreate,
12
+ PermissionRead,
13
+ PermissionUpdate,
14
+ UserPermissionCreate,
15
+ UserPermissionRead,
16
+ PermissionRouteCreate
17
+ )
18
+ from ..schemas.permission_category_schema import (
19
+ PermissionCategoryCreate,
20
+ PermissionCategoryRead
21
+ )
22
+ from ..services import permission_service
23
+ from oauth2fast_fastapi.dependencies import get_auth_session
24
+
25
+ router = APIRouter(
26
+ prefix="/permissions",
27
+ tags=["Permissions"],
28
+ )
29
+
30
+
31
+ # Categories
32
+
33
+
34
+ @router.post("/categories", response_model=PermissionCategoryRead)
35
+ async def create_category(
36
+ category_data: PermissionCategoryCreate,
37
+ session: AsyncSession = Depends(get_auth_session),
38
+ ):
39
+ """Create a new permission category."""
40
+ try:
41
+ category = await permission_service.create_category(category_data, session)
42
+ return PermissionCategoryRead.model_validate(category)
43
+ except ValueError as e:
44
+ raise HTTPException(status_code=400, detail=str(e))
45
+
46
+
47
+ @router.get("/categories", response_model=list[PermissionCategoryRead])
48
+ async def list_categories(
49
+ session: AsyncSession = Depends(get_auth_session),
50
+ skip: int = 0,
51
+ limit: int = 100,
52
+ ):
53
+ """List all categories."""
54
+ categories = await permission_service.list_categories(session, skip, limit)
55
+ return [PermissionCategoryRead.model_validate(c) for c in categories]
56
+
57
+
58
+ # Permissions CRUD
59
+
60
+
61
+ @router.post("/", response_model=PermissionRead)
62
+ async def create_permission(
63
+ permission_data: PermissionCreate,
64
+ session: AsyncSession = Depends(get_auth_session),
65
+ ):
66
+ """Create a new permission."""
67
+ try:
68
+ permission = await permission_service.create_permission(
69
+ permission_data, session
70
+ )
71
+ return PermissionRead.model_validate(permission)
72
+ except ValueError as e:
73
+ raise HTTPException(status_code=400, detail=str(e))
74
+
75
+
76
+ @router.get("/", response_model=list[PermissionRead])
77
+ async def list_permissions(
78
+ session: AsyncSession = Depends(get_auth_session),
79
+ skip: int = 0,
80
+ limit: int = 100,
81
+ ):
82
+ """List all permissions."""
83
+ permissions = await permission_service.list_permissions(session, skip, limit)
84
+ return [PermissionRead.model_validate(p) for p in permissions]
85
+
86
+
87
+ @router.get("/{permission_id}", response_model=PermissionRead)
88
+ async def get_permission(
89
+ permission_id: int,
90
+ session: AsyncSession = Depends(get_auth_session),
91
+ ):
92
+ """Get permission by ID."""
93
+ permission = await permission_service.get_permission(permission_id, session)
94
+ if not permission:
95
+ raise HTTPException(status_code=404, detail="Permission not found")
96
+ return PermissionRead.model_validate(permission)
97
+
98
+
99
+ @router.put("/{permission_id}", response_model=PermissionRead)
100
+ async def update_permission(
101
+ permission_id: int,
102
+ permission_data: PermissionUpdate,
103
+ session: AsyncSession = Depends(get_auth_session),
104
+ ):
105
+ """Update a permission."""
106
+ permission = await permission_service.update_permission(
107
+ permission_id, permission_data, session
108
+ )
109
+ if not permission:
110
+ raise HTTPException(status_code=404, detail="Permission not found")
111
+ return PermissionRead.model_validate(permission)
112
+
113
+
114
+ @router.delete("/{permission_id}")
115
+ async def delete_permission(
116
+ permission_id: int,
117
+ session: AsyncSession = Depends(get_auth_session),
118
+ ):
119
+ """Delete a permission."""
120
+ success = await permission_service.delete_permission(permission_id, session)
121
+ if not success:
122
+ raise HTTPException(status_code=404, detail="Permission not found")
123
+ return {"message": "Permission deleted successfully"}
124
+
125
+
126
+ # Permission Routes (Link)
127
+
128
+
129
+ @router.post("/{permission_id}/routes")
130
+ async def add_permission_route(
131
+ permission_id: int,
132
+ route_data: PermissionRouteCreate,
133
+ session: AsyncSession = Depends(get_auth_session),
134
+ ):
135
+ """Link a permission to a route."""
136
+ # Ensure permission_id matches URL
137
+ if route_data.permission_id != permission_id:
138
+ route_data.permission_id = permission_id
139
+
140
+ try:
141
+ await permission_service.add_permission_route(
142
+ permission_id, route_data.route_id, session
143
+ )
144
+ return {"message": "Route linked to permission successfully"}
145
+ except ValueError as e:
146
+ raise HTTPException(status_code=400, detail=str(e))
147
+
148
+
149
+ # User Permissions (Direct Assignment)
150
+
151
+
152
+ @router.post("/assign", response_model=UserPermissionRead)
153
+ async def assign_user_permission(
154
+ assignment_data: UserPermissionCreate,
155
+ session: AsyncSession = Depends(get_auth_session),
156
+ ):
157
+ """Assign a permission directly to a user."""
158
+ try:
159
+ user_perm = await permission_service.assign_user_permission(
160
+ assignment_data.user_id, assignment_data.permission_id, session
161
+ )
162
+ return UserPermissionRead(
163
+ permission_id=user_perm.permission_id,
164
+ entity_type=user_perm.entity_type,
165
+ entity_id=user_perm.entity_id
166
+ )
167
+ except ValueError as e:
168
+ raise HTTPException(status_code=400, detail=str(e))
169
+
170
+
171
+ @router.get("/user/{user_id}", response_model=list[PermissionRead])
172
+ async def list_user_permissions(
173
+ user_id: int,
174
+ session: AsyncSession = Depends(get_auth_session),
175
+ ):
176
+ """List all permissions assigned directly to a user."""
177
+ permissions = await permission_service.list_user_permissions(user_id, session)
178
+ return [PermissionRead.model_validate(p) for p in permissions]
179
+
180
+
181
+ @router.delete("/user/{user_id}/permission/{permission_id}")
182
+ async def remove_user_permission(
183
+ user_id: int,
184
+ permission_id: int,
185
+ session: AsyncSession = Depends(get_auth_session),
186
+ ):
187
+ """Remove a direct permission from a user."""
188
+ success = await permission_service.remove_user_permission(user_id, permission_id, session)
189
+ if not success:
190
+ raise HTTPException(status_code=404, detail="User permission assignment not found")
191
+ return {"message": "Permission removed from user successfully"}