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.
@@ -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
+ ```
@@ -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,4 @@
1
+ from .models import Role
2
+ from .core import RBACEngine
3
+
4
+ __all__ = ["Role", "RBACEngine"]
@@ -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