roleflow 0.1.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.
- roleflow-0.1.1/PKG-INFO +102 -0
- roleflow-0.1.1/README.md +84 -0
- roleflow-0.1.1/pyproject.toml +27 -0
- roleflow-0.1.1/src/easy_rbac/__init__.py +4 -0
- roleflow-0.1.1/src/easy_rbac/core.py +65 -0
- roleflow-0.1.1/src/easy_rbac/fastapi.py +39 -0
- roleflow-0.1.1/src/easy_rbac/models.py +17 -0
- roleflow-0.1.1/tests/__init__.py +1 -0
- roleflow-0.1.1/tests/test_core.py +40 -0
- roleflow-0.1.1/tests/test_fastapi.py +54 -0
- roleflow-0.1.1/tests/test_loader.py +33 -0
roleflow-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: roleflow
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A lightweight, hassle-free and production-ready RBAC (Role-Based Access Control) library.
|
|
5
|
+
Project-URL: Homepage, https://github.com/developer/easy-rbac
|
|
6
|
+
Author-email: sougata <sougatachongder8@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Provides-Extra: fastapi
|
|
11
|
+
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: fastapi; extra == 'test'
|
|
14
|
+
Requires-Dist: httpx; extra == 'test'
|
|
15
|
+
Requires-Dist: pytest; extra == 'test'
|
|
16
|
+
Requires-Dist: pytest-asyncio; extra == 'test'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Easy RBAC
|
|
20
|
+
|
|
21
|
+
A lightweight, production-ready Role-Based Access Control (RBAC) package for Python, designed to be simple, fast, and framework-agnostic, while featuring seamless integration out-of-the-box for FastAPI.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
- **Generic RBAC Engine**: Easily verify permissions using wildcards (`*`, `table.*`) or exact matches.
|
|
25
|
+
- **Pydantic Validation**: Strong typing and validation for your Role and Permission schemas.
|
|
26
|
+
- **FastAPI Integration**: Native `RBACGuard` dependency injection for secure and hassle-free route protection.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install easy-rbac
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To install with FastAPI dependencies:
|
|
35
|
+
```bash
|
|
36
|
+
pip install easy-rbac[fastapi]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Define your Roles
|
|
42
|
+
```python
|
|
43
|
+
from easy_rbac import Role, RBACEngine
|
|
44
|
+
|
|
45
|
+
roles = [
|
|
46
|
+
Role(id=1, name="ROLE_ADMIN", permissions=["*"]),
|
|
47
|
+
Role(id=2, name="ROLE_STUDENT", permissions=["profile.read", "profile.edit", "course.read"]),
|
|
48
|
+
Role(id=3, name="ROLE_HOD", permissions=["course.*", "leave.approve"])
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
engine = RBACEngine(roles=roles)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Fetch Roles from a Database (Dynamic Loading)
|
|
55
|
+
You don't have to provide all roles upfront. You can hook into your Database ORM by passing a `role_loader` callback function to the engine:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from easy_rbac import Role, RBACEngine
|
|
59
|
+
|
|
60
|
+
# Simulated database fetch function (e.g. using SQLAlchemy)
|
|
61
|
+
def db_role_loader(role_name: str) -> Role:
|
|
62
|
+
# 1. Query your database here using SQLAlchemy
|
|
63
|
+
# db_record = session.query(DbRole).filter(DbRole.name == role_name).first()
|
|
64
|
+
# 2. Convert database result into the generic easy_rbac.Role schema
|
|
65
|
+
# return Role(id=db_record.id, name=db_record.name, permissions=db_record.permissions)
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Initialize engine without static roles
|
|
69
|
+
engine = RBACEngine(role_loader=db_role_loader)
|
|
70
|
+
|
|
71
|
+
# The engine will automatically call db_role_loader("ROLE_ADMIN") and cache it!
|
|
72
|
+
engine.is_granted("ROLE_ADMIN", "table1.read")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Check Permissions
|
|
76
|
+
```python
|
|
77
|
+
# Returns True
|
|
78
|
+
engine.is_granted("ROLE_ADMIN", "anything.you.want")
|
|
79
|
+
engine.is_granted("ROLE_STUDENT", "profile.read")
|
|
80
|
+
engine.is_granted("ROLE_HOD", "course.create")
|
|
81
|
+
|
|
82
|
+
# Returns False
|
|
83
|
+
engine.is_granted("ROLE_STUDENT", "course.create")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. FastAPI Integration
|
|
87
|
+
```python
|
|
88
|
+
from fastapi import FastAPI, Depends
|
|
89
|
+
from easy_rbac.fastapi import RBACGuard
|
|
90
|
+
|
|
91
|
+
app = FastAPI()
|
|
92
|
+
|
|
93
|
+
# A mock function to get the current user's role
|
|
94
|
+
def get_current_user_role() -> str:
|
|
95
|
+
return "ROLE_STUDENT"
|
|
96
|
+
|
|
97
|
+
guard = RBACGuard(engine=engine, role_provider=get_current_user_role)
|
|
98
|
+
|
|
99
|
+
@app.get("/courses", dependencies=[Depends(guard("course.read"))])
|
|
100
|
+
def list_courses():
|
|
101
|
+
return {"message": "You can read courses!"}
|
|
102
|
+
```
|
roleflow-0.1.1/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Easy RBAC
|
|
2
|
+
|
|
3
|
+
A lightweight, production-ready Role-Based Access Control (RBAC) package for Python, designed to be simple, fast, and framework-agnostic, while featuring seamless integration out-of-the-box for FastAPI.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Generic RBAC Engine**: Easily verify permissions using wildcards (`*`, `table.*`) or exact matches.
|
|
7
|
+
- **Pydantic Validation**: Strong typing and validation for your Role and Permission schemas.
|
|
8
|
+
- **FastAPI Integration**: Native `RBACGuard` dependency injection for secure and hassle-free route protection.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install easy-rbac
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
To install with FastAPI dependencies:
|
|
17
|
+
```bash
|
|
18
|
+
pip install easy-rbac[fastapi]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Define your Roles
|
|
24
|
+
```python
|
|
25
|
+
from easy_rbac import Role, RBACEngine
|
|
26
|
+
|
|
27
|
+
roles = [
|
|
28
|
+
Role(id=1, name="ROLE_ADMIN", permissions=["*"]),
|
|
29
|
+
Role(id=2, name="ROLE_STUDENT", permissions=["profile.read", "profile.edit", "course.read"]),
|
|
30
|
+
Role(id=3, name="ROLE_HOD", permissions=["course.*", "leave.approve"])
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
engine = RBACEngine(roles=roles)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Fetch Roles from a Database (Dynamic Loading)
|
|
37
|
+
You don't have to provide all roles upfront. You can hook into your Database ORM by passing a `role_loader` callback function to the engine:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from easy_rbac import Role, RBACEngine
|
|
41
|
+
|
|
42
|
+
# Simulated database fetch function (e.g. using SQLAlchemy)
|
|
43
|
+
def db_role_loader(role_name: str) -> Role:
|
|
44
|
+
# 1. Query your database here using SQLAlchemy
|
|
45
|
+
# db_record = session.query(DbRole).filter(DbRole.name == role_name).first()
|
|
46
|
+
# 2. Convert database result into the generic easy_rbac.Role schema
|
|
47
|
+
# return Role(id=db_record.id, name=db_record.name, permissions=db_record.permissions)
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
# Initialize engine without static roles
|
|
51
|
+
engine = RBACEngine(role_loader=db_role_loader)
|
|
52
|
+
|
|
53
|
+
# The engine will automatically call db_role_loader("ROLE_ADMIN") and cache it!
|
|
54
|
+
engine.is_granted("ROLE_ADMIN", "table1.read")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. Check Permissions
|
|
58
|
+
```python
|
|
59
|
+
# Returns True
|
|
60
|
+
engine.is_granted("ROLE_ADMIN", "anything.you.want")
|
|
61
|
+
engine.is_granted("ROLE_STUDENT", "profile.read")
|
|
62
|
+
engine.is_granted("ROLE_HOD", "course.create")
|
|
63
|
+
|
|
64
|
+
# Returns False
|
|
65
|
+
engine.is_granted("ROLE_STUDENT", "course.create")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. FastAPI Integration
|
|
69
|
+
```python
|
|
70
|
+
from fastapi import FastAPI, Depends
|
|
71
|
+
from easy_rbac.fastapi import RBACGuard
|
|
72
|
+
|
|
73
|
+
app = FastAPI()
|
|
74
|
+
|
|
75
|
+
# A mock function to get the current user's role
|
|
76
|
+
def get_current_user_role() -> str:
|
|
77
|
+
return "ROLE_STUDENT"
|
|
78
|
+
|
|
79
|
+
guard = RBACGuard(engine=engine, role_provider=get_current_user_role)
|
|
80
|
+
|
|
81
|
+
@app.get("/courses", dependencies=[Depends(guard("course.read"))])
|
|
82
|
+
def list_courses():
|
|
83
|
+
return {"message": "You can read courses!"}
|
|
84
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "roleflow"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "A lightweight, hassle-free and production-ready RBAC (Role-Based Access Control) library."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "sougata", email = "sougatachongder8@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pydantic>=2.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
fastapi = ["fastapi>=0.100.0"]
|
|
21
|
+
test = ["pytest", "pytest-asyncio", "fastapi", "httpx"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/developer/easy-rbac"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/easy_rbac"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import List, Optional, Dict, Callable
|
|
2
|
+
from easy_rbac.models import Role
|
|
3
|
+
|
|
4
|
+
class RBACEngine:
|
|
5
|
+
"""
|
|
6
|
+
Core engine for Role-Based Access Control verification.
|
|
7
|
+
"""
|
|
8
|
+
def __init__(self, roles: Optional[List[Role]] = None, role_loader: Optional[Callable[[str], Optional[Role]]] = None):
|
|
9
|
+
"""
|
|
10
|
+
Initialize the RBAC engine.
|
|
11
|
+
:param roles: Optional list of Role models.
|
|
12
|
+
:param role_loader: Optional callback to dynamically fetch a role (e.g., from a database) if it's not found in memory.
|
|
13
|
+
"""
|
|
14
|
+
self._roles: Dict[str, Role] = {}
|
|
15
|
+
self.role_loader = role_loader
|
|
16
|
+
if roles:
|
|
17
|
+
for role in roles:
|
|
18
|
+
self.add_role(role)
|
|
19
|
+
|
|
20
|
+
def add_role(self, role: Role) -> None:
|
|
21
|
+
"""Register a new role in the engine."""
|
|
22
|
+
self._roles[role.name] = role
|
|
23
|
+
|
|
24
|
+
def get_role(self, role_name: str) -> Optional[Role]:
|
|
25
|
+
"""Retrieve a registered role by its name. Falls back to role_loader if configured."""
|
|
26
|
+
if role_name in self._roles:
|
|
27
|
+
return self._roles[role_name]
|
|
28
|
+
|
|
29
|
+
# If not cached and we have a database/dynamic loader configured
|
|
30
|
+
if self.role_loader:
|
|
31
|
+
role = self.role_loader(role_name)
|
|
32
|
+
if role:
|
|
33
|
+
self.add_role(role) # Cache it for future queries
|
|
34
|
+
return role
|
|
35
|
+
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def is_granted(self, role_name: str, required_permission: str) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Verify if a role has the required permission.
|
|
41
|
+
|
|
42
|
+
:param role_name: The name of the role trying to access the resource.
|
|
43
|
+
:param required_permission: The permission string required for access (e.g. 'table1.read').
|
|
44
|
+
:return: True if access is granted, False otherwise.
|
|
45
|
+
"""
|
|
46
|
+
role = self.get_role(role_name)
|
|
47
|
+
if not role:
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
for permit in role.permissions:
|
|
51
|
+
# 1. Root wildcard match
|
|
52
|
+
if permit == "*":
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
# 2. Exact match
|
|
56
|
+
if permit == required_permission:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
# 3. Domain wildcard match (e.g., "table1.*" matches "table1.read")
|
|
60
|
+
if permit.endswith(".*"):
|
|
61
|
+
domain = permit[:-2] # remove the '.*'
|
|
62
|
+
if required_permission.startswith(f"{domain}."):
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
return False
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Callable, Optional, Any
|
|
2
|
+
from easy_rbac.core import RBACEngine
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from fastapi import HTTPException, status
|
|
6
|
+
from fastapi.requests import Request
|
|
7
|
+
HAS_FASTAPI = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
HAS_FASTAPI = False
|
|
10
|
+
|
|
11
|
+
class RBACGuard:
|
|
12
|
+
"""
|
|
13
|
+
A dependency injection factory for FastAPI to secure routes using Role-Based Access Control.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, engine: RBACEngine, role_provider: Callable[..., str]):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the RBACGuard.
|
|
18
|
+
|
|
19
|
+
:param engine: An instance of RBACEngine.
|
|
20
|
+
:param role_provider: A FastAPI dependency (callable) that resolves and returns the current user's role name as a string.
|
|
21
|
+
"""
|
|
22
|
+
if not HAS_FASTAPI:
|
|
23
|
+
raise ImportError("FastAPI is not installed. Please install easy-rbac[fastapi] to use RBACGuard.")
|
|
24
|
+
|
|
25
|
+
self.engine = engine
|
|
26
|
+
self.role_provider = role_provider
|
|
27
|
+
|
|
28
|
+
def __call__(self, required_permission: str):
|
|
29
|
+
"""
|
|
30
|
+
Returns a FastAPI dependency function to check for a specific permission.
|
|
31
|
+
"""
|
|
32
|
+
def dependency(role_name: str = __import__("fastapi").Depends(self.role_provider)):
|
|
33
|
+
if not self.engine.is_granted(role_name, required_permission):
|
|
34
|
+
raise HTTPException(
|
|
35
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
36
|
+
detail=f"Access denied: Requires permission '{required_permission}'"
|
|
37
|
+
)
|
|
38
|
+
return role_name
|
|
39
|
+
return dependency
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
class Role(BaseModel):
|
|
5
|
+
"""
|
|
6
|
+
Data model representing an RBAC Role.
|
|
7
|
+
"""
|
|
8
|
+
id: Union[int, str] = Field(..., description="Unique identifier for the role.")
|
|
9
|
+
name: str = Field(..., description="Unique name of the role (e.g., 'ROLE_ADMIN').")
|
|
10
|
+
permissions: List[str] = Field(
|
|
11
|
+
default_factory=list,
|
|
12
|
+
description="List of permission strings. Supports exact match and wildcards (e.g. '*' or 'domain.*')."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
model_config = {
|
|
16
|
+
"frozen": True
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Tests for easy-rbac
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from easy_rbac.models import Role
|
|
3
|
+
from easy_rbac.core import RBACEngine
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def rbac_engine():
|
|
7
|
+
roles = [
|
|
8
|
+
Role(id=1, name="ROLE_ADMIN", permissions=["*"]),
|
|
9
|
+
Role(id=2, name="ROLE_STUDENT", permissions=["profile.read", "profile.edit", "course.read"]),
|
|
10
|
+
Role(id=3, name="ROLE_HOD", permissions=["course.*", "leave.approve"])
|
|
11
|
+
]
|
|
12
|
+
return RBACEngine(roles=roles)
|
|
13
|
+
|
|
14
|
+
def test_admin_root_access(rbac_engine):
|
|
15
|
+
assert rbac_engine.is_granted("ROLE_ADMIN", "anything") is True
|
|
16
|
+
assert rbac_engine.is_granted("ROLE_ADMIN", "table.read") is True
|
|
17
|
+
|
|
18
|
+
def test_student_exact_match(rbac_engine):
|
|
19
|
+
assert rbac_engine.is_granted("ROLE_STUDENT", "profile.read") is True
|
|
20
|
+
assert rbac_engine.is_granted("ROLE_STUDENT", "profile.edit") is True
|
|
21
|
+
assert rbac_engine.is_granted("ROLE_STUDENT", "course.read") is True
|
|
22
|
+
|
|
23
|
+
def test_student_denied_access(rbac_engine):
|
|
24
|
+
assert rbac_engine.is_granted("ROLE_STUDENT", "course.create") is False
|
|
25
|
+
assert rbac_engine.is_granted("ROLE_STUDENT", "leave.approve") is False
|
|
26
|
+
|
|
27
|
+
def test_hod_wildcard_domain_access(rbac_engine):
|
|
28
|
+
assert rbac_engine.is_granted("ROLE_HOD", "course.create") is True
|
|
29
|
+
assert rbac_engine.is_granted("ROLE_HOD", "course.delete") is True
|
|
30
|
+
assert rbac_engine.is_granted("ROLE_HOD", "course.anything") is True
|
|
31
|
+
|
|
32
|
+
def test_hod_exact_match(rbac_engine):
|
|
33
|
+
assert rbac_engine.is_granted("ROLE_HOD", "leave.approve") is True
|
|
34
|
+
|
|
35
|
+
def test_hod_denied_access(rbac_engine):
|
|
36
|
+
assert rbac_engine.is_granted("ROLE_HOD", "profile.read") is False
|
|
37
|
+
assert rbac_engine.is_granted("ROLE_HOD", "leave.reject") is False
|
|
38
|
+
|
|
39
|
+
def test_unknown_role(rbac_engine):
|
|
40
|
+
assert rbac_engine.is_granted("ROLE_UNKNOWN", "anything") is False
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fastapi import FastAPI, Depends
|
|
3
|
+
from fastapi.testclient import TestClient
|
|
4
|
+
from easy_rbac.models import Role
|
|
5
|
+
from easy_rbac.core import RBACEngine
|
|
6
|
+
from easy_rbac.fastapi import RBACGuard
|
|
7
|
+
|
|
8
|
+
# Setup simple roles
|
|
9
|
+
roles = [
|
|
10
|
+
Role(id=1, name="ROLE_STUDENT", permissions=["profile.read"]),
|
|
11
|
+
Role(id=2, name="ROLE_ADMIN", permissions=["*"])
|
|
12
|
+
]
|
|
13
|
+
engine = RBACEngine(roles=roles)
|
|
14
|
+
|
|
15
|
+
# Mocked state
|
|
16
|
+
current_user_role = "ROLE_STUDENT"
|
|
17
|
+
|
|
18
|
+
def get_current_role() -> str:
|
|
19
|
+
return current_user_role
|
|
20
|
+
|
|
21
|
+
guard = RBACGuard(engine=engine, role_provider=get_current_role)
|
|
22
|
+
|
|
23
|
+
app = FastAPI()
|
|
24
|
+
|
|
25
|
+
@app.get("/profile", dependencies=[Depends(guard("profile.read"))])
|
|
26
|
+
def get_profile():
|
|
27
|
+
return {"status": "ok"}
|
|
28
|
+
|
|
29
|
+
@app.get("/admin/settings", dependencies=[Depends(guard("settings.edit"))])
|
|
30
|
+
def edit_settings():
|
|
31
|
+
return {"status": "ok"}
|
|
32
|
+
|
|
33
|
+
client = TestClient(app)
|
|
34
|
+
|
|
35
|
+
def test_fastapi_allowed_access():
|
|
36
|
+
global current_user_role
|
|
37
|
+
current_user_role = "ROLE_STUDENT"
|
|
38
|
+
response = client.get("/profile")
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
assert response.json() == {"status": "ok"}
|
|
41
|
+
|
|
42
|
+
def test_fastapi_denied_access():
|
|
43
|
+
global current_user_role
|
|
44
|
+
current_user_role = "ROLE_STUDENT"
|
|
45
|
+
response = client.get("/admin/settings")
|
|
46
|
+
assert response.status_code == 403
|
|
47
|
+
assert response.json() == {"detail": "Access denied: Requires permission 'settings.edit'"}
|
|
48
|
+
|
|
49
|
+
def test_fastapi_admin_access():
|
|
50
|
+
global current_user_role
|
|
51
|
+
current_user_role = "ROLE_ADMIN"
|
|
52
|
+
response = client.get("/admin/settings")
|
|
53
|
+
assert response.status_code == 200
|
|
54
|
+
assert response.json() == {"status": "ok"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from easy_rbac.models import Role
|
|
3
|
+
from easy_rbac.core import RBACEngine
|
|
4
|
+
|
|
5
|
+
# Simulated Database
|
|
6
|
+
MOCK_DB = {
|
|
7
|
+
"ROLE_SUPER_ADMIN": {"id": 99, "name": "ROLE_SUPER_ADMIN", "permissions": ["*"]},
|
|
8
|
+
"ROLE_GUEST": {"id": 100, "name": "ROLE_GUEST", "permissions": ["guest.read"]}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def db_role_loader(role_name: str) -> Role | None:
|
|
12
|
+
"""Mock DB loader that fetches the role by name."""
|
|
13
|
+
record = MOCK_DB.get(role_name)
|
|
14
|
+
if record:
|
|
15
|
+
return Role(**record)
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
def test_dynamic_role_loading():
|
|
19
|
+
# We init without passing any static roles
|
|
20
|
+
engine = RBACEngine(role_loader=db_role_loader)
|
|
21
|
+
|
|
22
|
+
# It should dynamically fetch ROLE_SUPER_ADMIN from MOCK_DB
|
|
23
|
+
assert engine.is_granted("ROLE_SUPER_ADMIN", "anything.you.want") is True
|
|
24
|
+
|
|
25
|
+
# It should cache the role after the first load
|
|
26
|
+
assert "ROLE_SUPER_ADMIN" in engine._roles
|
|
27
|
+
|
|
28
|
+
# Test Guest
|
|
29
|
+
assert engine.is_granted("ROLE_GUEST", "guest.read") is True
|
|
30
|
+
assert engine.is_granted("ROLE_GUEST", "admin.write") is False
|
|
31
|
+
|
|
32
|
+
# Test unknown role
|
|
33
|
+
assert engine.is_granted("ROLE_UNKNOWN", "anything") is False
|