simple-module-permissions 0.0.1__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.
- simple_module_permissions-0.0.1/.gitignore +59 -0
- simple_module_permissions-0.0.1/LICENSE +21 -0
- simple_module_permissions-0.0.1/PKG-INFO +83 -0
- simple_module_permissions-0.0.1/README.md +54 -0
- simple_module_permissions-0.0.1/package.json +16 -0
- simple_module_permissions-0.0.1/permissions/__init__.py +1 -0
- simple_module_permissions-0.0.1/permissions/constants.py +13 -0
- simple_module_permissions-0.0.1/permissions/contracts/__init__.py +21 -0
- simple_module_permissions-0.0.1/permissions/contracts/schemas.py +65 -0
- simple_module_permissions-0.0.1/permissions/deps.py +72 -0
- simple_module_permissions-0.0.1/permissions/endpoints/__init__.py +0 -0
- simple_module_permissions-0.0.1/permissions/endpoints/api.py +102 -0
- simple_module_permissions-0.0.1/permissions/endpoints/views.py +123 -0
- simple_module_permissions-0.0.1/permissions/locales/en.json +64 -0
- simple_module_permissions-0.0.1/permissions/models.py +70 -0
- simple_module_permissions-0.0.1/permissions/module.py +54 -0
- simple_module_permissions-0.0.1/permissions/pages/RoleEdit.tsx +167 -0
- simple_module_permissions-0.0.1/permissions/pages/UserEdit.tsx +187 -0
- simple_module_permissions-0.0.1/permissions/py.typed +0 -0
- simple_module_permissions-0.0.1/permissions/service.py +280 -0
- simple_module_permissions-0.0.1/pyproject.toml +55 -0
- simple_module_permissions-0.0.1/tests/test_permissions_module.py +287 -0
- simple_module_permissions-0.0.1/tsconfig.json +11 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# UV
|
|
11
|
+
uv.lock
|
|
12
|
+
|
|
13
|
+
# Node
|
|
14
|
+
node_modules/
|
|
15
|
+
|
|
16
|
+
# IDE
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
*.swp
|
|
20
|
+
*.swo
|
|
21
|
+
|
|
22
|
+
# Environment
|
|
23
|
+
.env
|
|
24
|
+
|
|
25
|
+
# Database
|
|
26
|
+
*.db
|
|
27
|
+
*.sqlite3
|
|
28
|
+
|
|
29
|
+
# Module-managed runtime state (e.g. uploaded dataset files,
|
|
30
|
+
# default storage_dir for SM_DATASETS_STORAGE_DIR).
|
|
31
|
+
var/
|
|
32
|
+
|
|
33
|
+
# file_storage filesystem backend default root (override via SM_FILE_STORAGE_FS_ROOT_PATH).
|
|
34
|
+
uploads/
|
|
35
|
+
|
|
36
|
+
# Vite
|
|
37
|
+
host/static/dist/
|
|
38
|
+
|
|
39
|
+
# Auto-generated frontend module manifest (regenerated by the host at boot
|
|
40
|
+
# or via `make gen-pages`).
|
|
41
|
+
host/client_app/modules.manifest.json
|
|
42
|
+
host/client_app/modules.generated.ts
|
|
43
|
+
host/client_app/modules.generated.css
|
|
44
|
+
|
|
45
|
+
# Worktrees
|
|
46
|
+
.worktrees/
|
|
47
|
+
|
|
48
|
+
# Performance profiles
|
|
49
|
+
.memray/
|
|
50
|
+
.benchmarks/
|
|
51
|
+
|
|
52
|
+
# OS
|
|
53
|
+
.DS_Store
|
|
54
|
+
Thumbs.db
|
|
55
|
+
|
|
56
|
+
.playwright-cli/*
|
|
57
|
+
.playwright-mcp/*
|
|
58
|
+
host/client_app/.playwright-cli/*
|
|
59
|
+
.superpowers/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anto Subash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_module_permissions
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: RBAC primitives — roles, permissions, @require_permission decorator, admin UI for simple_module
|
|
5
|
+
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
7
|
+
Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: authorization,permissions,rbac,simple-module
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: simple-module-core==0.0.1
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.1
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.1
|
|
27
|
+
Requires-Dist: simple-module-users==0.0.1
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# simple_module_permissions
|
|
31
|
+
|
|
32
|
+
Role-based access control (RBAC) for [simple_module](https://github.com/antosubash/simple_module_python) apps. Users get roles, roles carry permissions, and route handlers declare required permissions at the decorator or dependency layer.
|
|
33
|
+
|
|
34
|
+
Pre-wired into any app scaffolded with `simple-module new`.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install simple_module_permissions
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What it provides
|
|
43
|
+
|
|
44
|
+
- `Role` and `Permission` SQLModel tables, seeded from module-registered defaults.
|
|
45
|
+
- `@require_permission("orders.read")` route decorator and `HasPermission("...")` dependency.
|
|
46
|
+
- Admin UI at `/permissions/admin` for assigning roles to users.
|
|
47
|
+
- `register_permissions()` hook — every module declares its permission strings at boot, the registry dedupes and persists them.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Declare permissions at module boot:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# modules/orders/orders/module.py
|
|
55
|
+
class OrdersModule(ModuleBase):
|
|
56
|
+
meta = ModuleMeta(name="orders")
|
|
57
|
+
|
|
58
|
+
def register_permissions(self):
|
|
59
|
+
return ["orders.read", "orders.write"]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Guard a route:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from fastapi import APIRouter, Depends
|
|
66
|
+
from permissions.deps import HasPermission # type: ignore[import-not-found]
|
|
67
|
+
|
|
68
|
+
router = APIRouter()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.get("/orders", dependencies=[Depends(HasPermission("orders.read"))])
|
|
72
|
+
async def list_orders(): ...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Admin flow: navigate to `/permissions/admin`, create a role, assign permissions, assign the role to users.
|
|
76
|
+
|
|
77
|
+
## Depends on
|
|
78
|
+
|
|
79
|
+
- `simple_module_core`, `simple_module_db`, `simple_module_hosting`, `simple_module_users`
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# simple_module_permissions
|
|
2
|
+
|
|
3
|
+
Role-based access control (RBAC) for [simple_module](https://github.com/antosubash/simple_module_python) apps. Users get roles, roles carry permissions, and route handlers declare required permissions at the decorator or dependency layer.
|
|
4
|
+
|
|
5
|
+
Pre-wired into any app scaffolded with `simple-module new`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install simple_module_permissions
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What it provides
|
|
14
|
+
|
|
15
|
+
- `Role` and `Permission` SQLModel tables, seeded from module-registered defaults.
|
|
16
|
+
- `@require_permission("orders.read")` route decorator and `HasPermission("...")` dependency.
|
|
17
|
+
- Admin UI at `/permissions/admin` for assigning roles to users.
|
|
18
|
+
- `register_permissions()` hook — every module declares its permission strings at boot, the registry dedupes and persists them.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Declare permissions at module boot:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# modules/orders/orders/module.py
|
|
26
|
+
class OrdersModule(ModuleBase):
|
|
27
|
+
meta = ModuleMeta(name="orders")
|
|
28
|
+
|
|
29
|
+
def register_permissions(self):
|
|
30
|
+
return ["orders.read", "orders.write"]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Guard a route:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from fastapi import APIRouter, Depends
|
|
37
|
+
from permissions.deps import HasPermission # type: ignore[import-not-found]
|
|
38
|
+
|
|
39
|
+
router = APIRouter()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/orders", dependencies=[Depends(HasPermission("orders.read"))])
|
|
43
|
+
async def list_orders(): ...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Admin flow: navigate to `/permissions/admin`, create a role, assign permissions, assign the role to users.
|
|
47
|
+
|
|
48
|
+
## Depends on
|
|
49
|
+
|
|
50
|
+
- `simple_module_core`, `simple_module_db`, `simple_module_hosting`, `simple_module_users`
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simple-module-py/permissions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Frontend assets for the Permissions module",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^19.0.0",
|
|
8
|
+
"react-dom": "^19.0.0",
|
|
9
|
+
"@inertiajs/react": "^2.0.0",
|
|
10
|
+
"@simple-module-py/ui": "*"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@simple-module-py/tsconfig": "*"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Permissions module."""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Permission keys declared by this module.
|
|
2
|
+
|
|
3
|
+
Centralised so `register_permissions`, endpoint guards, and tests all
|
|
4
|
+
reference the same literal. Other modules that need to check one of these
|
|
5
|
+
permissions should import the constant rather than duplicating the string.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
PERMISSION_GROUP = "Permissions"
|
|
11
|
+
|
|
12
|
+
PERM_VIEW = "permissions.view"
|
|
13
|
+
PERM_MANAGE = "permissions.manage"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Permissions contracts — public interface for other modules."""
|
|
2
|
+
|
|
3
|
+
from permissions.contracts.schemas import (
|
|
4
|
+
PermissionGroupOut,
|
|
5
|
+
RoleOut,
|
|
6
|
+
RolePermissionsOut,
|
|
7
|
+
RolePermissionsUpdate,
|
|
8
|
+
UserOut,
|
|
9
|
+
UserPermissionsOut,
|
|
10
|
+
UserPermissionsUpdate,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PermissionGroupOut",
|
|
15
|
+
"RoleOut",
|
|
16
|
+
"RolePermissionsOut",
|
|
17
|
+
"RolePermissionsUpdate",
|
|
18
|
+
"UserOut",
|
|
19
|
+
"UserPermissionsOut",
|
|
20
|
+
"UserPermissionsUpdate",
|
|
21
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""SQLModel DTOs for the Permissions module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from pydantic import ConfigDict
|
|
8
|
+
from sqlmodel import Field, SQLModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PermissionGroupOut(SQLModel):
|
|
12
|
+
"""A named group of related permission keys (one per module)."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
permissions: list[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RoleOut(SQLModel):
|
|
19
|
+
"""A role as surfaced by the permissions admin UI."""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(from_attributes=True)
|
|
22
|
+
|
|
23
|
+
id: uuid.UUID
|
|
24
|
+
name: str
|
|
25
|
+
description: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RolePermissionsOut(SQLModel):
|
|
29
|
+
"""A role together with its currently assigned permission keys."""
|
|
30
|
+
|
|
31
|
+
role: RoleOut
|
|
32
|
+
permissions: list[str]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RolePermissionsUpdate(SQLModel):
|
|
36
|
+
"""Replace the full set of permission keys assigned to a role."""
|
|
37
|
+
|
|
38
|
+
permissions: list[str] = Field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UserOut(SQLModel):
|
|
42
|
+
"""A user as surfaced by the permissions admin UI."""
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(from_attributes=True)
|
|
45
|
+
|
|
46
|
+
id: uuid.UUID
|
|
47
|
+
email: str
|
|
48
|
+
full_name: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UserPermissionsOut(SQLModel):
|
|
52
|
+
"""A user together with their direct and role-inherited permission keys."""
|
|
53
|
+
|
|
54
|
+
user: UserOut
|
|
55
|
+
roles: list[str]
|
|
56
|
+
direct: list[str]
|
|
57
|
+
"""Keys granted directly to this user."""
|
|
58
|
+
inherited: list[str]
|
|
59
|
+
"""Keys the user holds via any of their roles (excluding duplicates of ``direct``)."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UserPermissionsUpdate(SQLModel):
|
|
63
|
+
"""Replace the full set of permission keys granted directly to a user."""
|
|
64
|
+
|
|
65
|
+
permissions: list[str] = Field(default_factory=list)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""FastAPI dependencies for the Permissions module.
|
|
2
|
+
|
|
3
|
+
In addition to the standard service wiring this module exports a
|
|
4
|
+
:class:`RequiresPermission` dependency that honours *both* role-based
|
|
5
|
+
and direct user grants — the framework's own
|
|
6
|
+
:class:`simple_module_hosting.permissions.RequiresPermission` checks
|
|
7
|
+
only roles, because the framework has no concept of user-direct grants.
|
|
8
|
+
Endpoints that want users to be able to hold individual permissions on
|
|
9
|
+
top of their roles should depend on this version instead.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
from fastapi import Depends, HTTPException, Request
|
|
17
|
+
from simple_module_core.permissions import WILDCARD, PermissionRegistry
|
|
18
|
+
from simple_module_db.deps import get_db
|
|
19
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
20
|
+
|
|
21
|
+
from permissions.service import PermissionService
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_permission_registry(request: Request) -> PermissionRegistry:
|
|
25
|
+
return request.app.state.sm.permissions
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def get_permission_service(
|
|
29
|
+
db: AsyncSession = Depends(get_db),
|
|
30
|
+
registry: PermissionRegistry = Depends(get_permission_registry),
|
|
31
|
+
) -> PermissionService:
|
|
32
|
+
return PermissionService(db, registry)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def assigned_by(request: Request) -> str | None:
|
|
36
|
+
"""Authenticated user id string, for audit columns."""
|
|
37
|
+
user = getattr(request.state, "user", None)
|
|
38
|
+
return str(user.id) if user is not None else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RequiresPermission:
|
|
42
|
+
"""FastAPI dependency enforcing a permission across roles *and* user grants.
|
|
43
|
+
|
|
44
|
+
Behaves like the framework's ``simple_module_hosting.RequiresPermission``
|
|
45
|
+
but additionally consults the ``permissions_user_permission`` table, so
|
|
46
|
+
a direct grant on a single user takes effect without inventing a role.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, permission: str) -> None:
|
|
50
|
+
self.permission = permission
|
|
51
|
+
|
|
52
|
+
async def __call__(
|
|
53
|
+
self,
|
|
54
|
+
request: Request,
|
|
55
|
+
service: PermissionService = Depends(get_permission_service),
|
|
56
|
+
) -> None:
|
|
57
|
+
user = getattr(request.state, "user", None)
|
|
58
|
+
if user is None:
|
|
59
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
60
|
+
|
|
61
|
+
role_perms: set[str] = getattr(request.state, "resolved_permissions", set()) or set()
|
|
62
|
+
if WILDCARD in role_perms or self.permission in role_perms:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
direct = await service.get_user_direct_keys(uuid.UUID(str(user.id)))
|
|
66
|
+
if self.permission in direct:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
status_code=403,
|
|
71
|
+
detail=f"Permission required: {self.permission}",
|
|
72
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""REST API endpoints for Permissions administration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
8
|
+
|
|
9
|
+
from permissions.constants import PERM_MANAGE, PERM_VIEW
|
|
10
|
+
from permissions.contracts.schemas import (
|
|
11
|
+
PermissionGroupOut,
|
|
12
|
+
RolePermissionsOut,
|
|
13
|
+
RolePermissionsUpdate,
|
|
14
|
+
UserPermissionsOut,
|
|
15
|
+
UserPermissionsUpdate,
|
|
16
|
+
)
|
|
17
|
+
from permissions.deps import RequiresPermission, assigned_by, get_permission_service
|
|
18
|
+
from permissions.service import PermissionService
|
|
19
|
+
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get(
|
|
24
|
+
"/",
|
|
25
|
+
response_model=list[PermissionGroupOut],
|
|
26
|
+
dependencies=[Depends(RequiresPermission(PERM_VIEW))],
|
|
27
|
+
)
|
|
28
|
+
async def list_registered(
|
|
29
|
+
service: PermissionService = Depends(get_permission_service),
|
|
30
|
+
) -> list[PermissionGroupOut]:
|
|
31
|
+
"""All permission keys auto-discovered from installed modules, grouped."""
|
|
32
|
+
return service.list_registered_groups()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Role-scoped endpoints ──────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get(
|
|
39
|
+
"/roles/{role_id}",
|
|
40
|
+
response_model=RolePermissionsOut,
|
|
41
|
+
dependencies=[Depends(RequiresPermission(PERM_VIEW))],
|
|
42
|
+
)
|
|
43
|
+
async def get_role_permissions(
|
|
44
|
+
role_id: uuid.UUID,
|
|
45
|
+
service: PermissionService = Depends(get_permission_service),
|
|
46
|
+
) -> RolePermissionsOut:
|
|
47
|
+
result = await service.get_role_permissions(role_id)
|
|
48
|
+
if result is None:
|
|
49
|
+
raise HTTPException(status_code=404, detail="Role not found")
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.put(
|
|
54
|
+
"/roles/{role_id}",
|
|
55
|
+
response_model=RolePermissionsOut,
|
|
56
|
+
dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
|
|
57
|
+
)
|
|
58
|
+
async def set_role_permissions(
|
|
59
|
+
role_id: uuid.UUID,
|
|
60
|
+
data: RolePermissionsUpdate,
|
|
61
|
+
request: Request,
|
|
62
|
+
service: PermissionService = Depends(get_permission_service),
|
|
63
|
+
) -> RolePermissionsOut:
|
|
64
|
+
result = await service.set_role_permissions(role_id, data.permissions, assigned_by(request))
|
|
65
|
+
if result is None:
|
|
66
|
+
raise HTTPException(status_code=404, detail="Role not found")
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── User-scoped endpoints ──────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get(
|
|
74
|
+
"/users/{user_id}",
|
|
75
|
+
response_model=UserPermissionsOut,
|
|
76
|
+
dependencies=[Depends(RequiresPermission(PERM_VIEW))],
|
|
77
|
+
)
|
|
78
|
+
async def get_user_permissions(
|
|
79
|
+
user_id: uuid.UUID,
|
|
80
|
+
service: PermissionService = Depends(get_permission_service),
|
|
81
|
+
) -> UserPermissionsOut:
|
|
82
|
+
result = await service.get_user_permissions(user_id)
|
|
83
|
+
if result is None:
|
|
84
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.put(
|
|
89
|
+
"/users/{user_id}",
|
|
90
|
+
response_model=UserPermissionsOut,
|
|
91
|
+
dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
|
|
92
|
+
)
|
|
93
|
+
async def set_user_permissions(
|
|
94
|
+
user_id: uuid.UUID,
|
|
95
|
+
data: UserPermissionsUpdate,
|
|
96
|
+
request: Request,
|
|
97
|
+
service: PermissionService = Depends(get_permission_service),
|
|
98
|
+
) -> UserPermissionsOut:
|
|
99
|
+
result = await service.set_user_permissions(user_id, data.permissions, assigned_by(request))
|
|
100
|
+
if result is None:
|
|
101
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
102
|
+
return result
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Inertia view endpoints for Permissions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, Request
|
|
8
|
+
from inertia import InertiaResponse
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
from simple_module_hosting.inertia_deps import InertiaDep
|
|
11
|
+
from simple_module_hosting.inertia_utils import (
|
|
12
|
+
redirect_back_with_errors,
|
|
13
|
+
validation_errors_to_dict,
|
|
14
|
+
)
|
|
15
|
+
from starlette.responses import RedirectResponse
|
|
16
|
+
|
|
17
|
+
from permissions.constants import PERM_MANAGE
|
|
18
|
+
from permissions.contracts.schemas import RolePermissionsUpdate, UserPermissionsUpdate
|
|
19
|
+
from permissions.deps import RequiresPermission, assigned_by, get_permission_service
|
|
20
|
+
from permissions.service import PermissionService
|
|
21
|
+
|
|
22
|
+
router = APIRouter()
|
|
23
|
+
|
|
24
|
+
_ADMIN_URL = "/users/admin"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@router.get("/", response_model=None)
|
|
28
|
+
async def browse() -> RedirectResponse:
|
|
29
|
+
return RedirectResponse(_ADMIN_URL, status_code=307)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Role edit ──────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get(
|
|
36
|
+
"/roles/{role_id}/edit",
|
|
37
|
+
response_model=None,
|
|
38
|
+
dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
|
|
39
|
+
)
|
|
40
|
+
async def edit_role(
|
|
41
|
+
role_id: uuid.UUID,
|
|
42
|
+
inertia: InertiaDep,
|
|
43
|
+
service: PermissionService = Depends(get_permission_service),
|
|
44
|
+
) -> InertiaResponse | RedirectResponse:
|
|
45
|
+
assignment = await service.get_role_permissions(role_id)
|
|
46
|
+
if assignment is None:
|
|
47
|
+
return RedirectResponse(_ADMIN_URL, status_code=303)
|
|
48
|
+
groups = service.list_registered_groups()
|
|
49
|
+
return await inertia.render(
|
|
50
|
+
"Permissions/RoleEdit",
|
|
51
|
+
{
|
|
52
|
+
"role": assignment.role.model_dump(mode="json"),
|
|
53
|
+
"assigned": assignment.permissions,
|
|
54
|
+
"groups": [g.model_dump(mode="json") for g in groups],
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.put(
|
|
60
|
+
"/roles/{role_id}",
|
|
61
|
+
response_model=None,
|
|
62
|
+
dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
|
|
63
|
+
)
|
|
64
|
+
async def update_role(
|
|
65
|
+
role_id: uuid.UUID,
|
|
66
|
+
request: Request,
|
|
67
|
+
service: PermissionService = Depends(get_permission_service),
|
|
68
|
+
) -> RedirectResponse:
|
|
69
|
+
body = await request.json()
|
|
70
|
+
try:
|
|
71
|
+
data = RolePermissionsUpdate(**body)
|
|
72
|
+
except ValidationError as exc:
|
|
73
|
+
return redirect_back_with_errors(request, validation_errors_to_dict(exc))
|
|
74
|
+
await service.set_role_permissions(role_id, data.permissions, assigned_by(request))
|
|
75
|
+
return RedirectResponse(_ADMIN_URL, status_code=303)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── User edit ──────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.get(
|
|
82
|
+
"/users/{user_id}/edit",
|
|
83
|
+
response_model=None,
|
|
84
|
+
dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
|
|
85
|
+
)
|
|
86
|
+
async def edit_user(
|
|
87
|
+
user_id: uuid.UUID,
|
|
88
|
+
inertia: InertiaDep,
|
|
89
|
+
service: PermissionService = Depends(get_permission_service),
|
|
90
|
+
) -> InertiaResponse | RedirectResponse:
|
|
91
|
+
assignment = await service.get_user_permissions(user_id)
|
|
92
|
+
if assignment is None:
|
|
93
|
+
return RedirectResponse(_ADMIN_URL, status_code=303)
|
|
94
|
+
groups = service.list_registered_groups()
|
|
95
|
+
return await inertia.render(
|
|
96
|
+
"Permissions/UserEdit",
|
|
97
|
+
{
|
|
98
|
+
"user": assignment.user.model_dump(mode="json"),
|
|
99
|
+
"roles": assignment.roles,
|
|
100
|
+
"direct": assignment.direct,
|
|
101
|
+
"inherited": assignment.inherited,
|
|
102
|
+
"groups": [g.model_dump(mode="json") for g in groups],
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.put(
|
|
108
|
+
"/users/{user_id}",
|
|
109
|
+
response_model=None,
|
|
110
|
+
dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
|
|
111
|
+
)
|
|
112
|
+
async def update_user(
|
|
113
|
+
user_id: uuid.UUID,
|
|
114
|
+
request: Request,
|
|
115
|
+
service: PermissionService = Depends(get_permission_service),
|
|
116
|
+
) -> RedirectResponse:
|
|
117
|
+
body = await request.json()
|
|
118
|
+
try:
|
|
119
|
+
data = UserPermissionsUpdate(**body)
|
|
120
|
+
except ValidationError as exc:
|
|
121
|
+
return redirect_back_with_errors(request, validation_errors_to_dict(exc))
|
|
122
|
+
await service.set_user_permissions(user_id, data.permissions, assigned_by(request))
|
|
123
|
+
return RedirectResponse(_ADMIN_URL, status_code=303)
|