abs-auth-rbac-core 0.3.1__tar.gz → 0.3.13__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.
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/PKG-INFO +4 -2
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/auth/__init__.py +15 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/auth/auth_functions.py +4 -1
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/auth/middleware.py +245 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/user.py +1 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/rbac/service.py +145 -51
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/repository/__init__.py +4 -0
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/repository/permission_repository.py +12 -0
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/repository/role_repository.py +18 -0
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/service/__init__.py +4 -0
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/service/permission_service.py +15 -0
- abs_auth_rbac_core-0.3.13/abs_auth_rbac_core/service/role_service.py +18 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/util/permission_constants.py +27 -1
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/pyproject.toml +4 -2
- abs_auth_rbac_core-0.3.1/abs_auth_rbac_core/auth/__init__.py +0 -3
- abs_auth_rbac_core-0.3.1/abs_auth_rbac_core/auth/middleware.py +0 -51
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/README.md +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/__init__.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/auth/jwt_functions.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/__init__.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/base_model.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/gov_casbin_rule.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/permissions.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/rbac_model.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/role_permission.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/roles.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/seeder/permission_seeder.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/user_permission.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/user_role.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/rbac/__init__.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/rbac/decorator.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/rbac/policy.conf +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/schema/__init__.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/schema/permission.py +0 -0
- {abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/util/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: abs-auth-rbac-core
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.13
|
|
4
4
|
Summary: RBAC and Auth core utilities including JWT token management.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: AutoBridgeSystems
|
|
@@ -12,7 +12,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Requires-Dist: abs-exception-core (>=0.2.0,<0.3.0)
|
|
15
|
-
Requires-Dist: abs-
|
|
15
|
+
Requires-Dist: abs-nosql-repository-core (>=0.11.8,<0.12.0)
|
|
16
|
+
Requires-Dist: abs-repository-core (>=0.3.0,<0.4.0)
|
|
17
|
+
Requires-Dist: abs-utils (>=0.4.1,<0.5.0)
|
|
16
18
|
Requires-Dist: casbin (>=1.41.0,<2.0.0)
|
|
17
19
|
Requires-Dist: casbin-redis-watcher (>=1.3.0,<2.0.0)
|
|
18
20
|
Requires-Dist: casbin-sqlalchemy-adapter (>=1.4.0,<2.0.0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .jwt_functions import JWTFunctions
|
|
2
|
+
from .middleware import (
|
|
3
|
+
CustomHTTPBearer,
|
|
4
|
+
auth_middleware,
|
|
5
|
+
contacts_auth_middleware,
|
|
6
|
+
unified_auth_middleware
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"JWTFunctions",
|
|
11
|
+
"CustomHTTPBearer",
|
|
12
|
+
"auth_middleware",
|
|
13
|
+
"contacts_auth_middleware",
|
|
14
|
+
"unified_auth_middleware"
|
|
15
|
+
]
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/auth/auth_functions.py
RENAMED
|
@@ -20,7 +20,10 @@ def get_user_by_attribute(db_session: Callable[...,Any],attribute: str, value: s
|
|
|
20
20
|
if not hasattr(Users, attribute):
|
|
21
21
|
raise ValidationError(detail=f"Attribute {attribute} does not exist on the User model")
|
|
22
22
|
|
|
23
|
-
user = session.query(Users).filter(
|
|
23
|
+
user = session.query(Users).filter(
|
|
24
|
+
getattr(Users, attribute) == value,
|
|
25
|
+
Users.deleted_at.is_(None) # Filter out soft-deleted users
|
|
26
|
+
).first()
|
|
24
27
|
|
|
25
28
|
if not user:
|
|
26
29
|
raise NotFoundError(detail="User not found")
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from fastapi import Depends, Request
|
|
2
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
3
|
+
from fastapi import HTTPException
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Callable, Any, Optional
|
|
6
|
+
|
|
7
|
+
from .jwt_functions import JWTFunctions
|
|
8
|
+
from .auth_functions import get_user_by_attribute
|
|
9
|
+
from abs_exception_core.exceptions import UnauthorizedError, AuthError, NotFoundError
|
|
10
|
+
from abs_nosql_repository_core.repository import BaseRepository
|
|
11
|
+
from fastapi.security.utils import get_authorization_scheme_param
|
|
12
|
+
|
|
13
|
+
class CustomHTTPBearer(HTTPBearer):
|
|
14
|
+
def __init__(self, **kwargs):
|
|
15
|
+
super().__init__(**kwargs)
|
|
16
|
+
|
|
17
|
+
async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
|
|
18
|
+
authorization = request.headers.get("Authorization")
|
|
19
|
+
scheme, credentials = get_authorization_scheme_param(authorization)
|
|
20
|
+
|
|
21
|
+
if not (authorization and scheme and credentials):
|
|
22
|
+
if self.auto_error:
|
|
23
|
+
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
24
|
+
else:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
if scheme.lower() != "bearer":
|
|
28
|
+
if self.auto_error:
|
|
29
|
+
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
30
|
+
else:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
|
34
|
+
|
|
35
|
+
security = CustomHTTPBearer()
|
|
36
|
+
# security = HTTPBearer()
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Dependency acting like per-route middleware
|
|
41
|
+
def auth_middleware(
|
|
42
|
+
db_session: Callable[...,Any],
|
|
43
|
+
jwt_secret_key:str,
|
|
44
|
+
jwt_algorithm:str
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
This middleware is used for authentication of the user.
|
|
48
|
+
Args:
|
|
49
|
+
db_session: Callable[...,Any]: Session of the SQLAlchemy database engine
|
|
50
|
+
jwt_secret_key: Secret key of the JWT for jwt functions
|
|
51
|
+
jwt_algorithm: Algorithm used for JWT
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
"""
|
|
55
|
+
async def get_auth(request: Request, token: HTTPAuthorizationCredentials = Depends(security)):
|
|
56
|
+
jwt_functions = JWTFunctions(secret_key=jwt_secret_key,algorithm=jwt_algorithm)
|
|
57
|
+
try:
|
|
58
|
+
if not token or not token.credentials:
|
|
59
|
+
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
60
|
+
|
|
61
|
+
payload = jwt_functions.get_data(token=token.credentials)
|
|
62
|
+
uuid = payload.get("uuid")
|
|
63
|
+
|
|
64
|
+
user = get_user_by_attribute(db_session=db_session,attribute="uuid", value=uuid)
|
|
65
|
+
|
|
66
|
+
if not user:
|
|
67
|
+
logger.error(f"Authentication failed: User with id {uuid} not found")
|
|
68
|
+
raise UnauthorizedError(detail="Authentication failed")
|
|
69
|
+
|
|
70
|
+
# Attach user to request state
|
|
71
|
+
request.state.user = user
|
|
72
|
+
return user
|
|
73
|
+
|
|
74
|
+
except UnauthorizedError as e:
|
|
75
|
+
logger.error(e)
|
|
76
|
+
raise
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Authentication error: {str(e)}", exc_info=True)
|
|
79
|
+
raise UnauthorizedError(detail="Authentication failed")
|
|
80
|
+
|
|
81
|
+
return get_auth
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def contacts_auth_middleware(
|
|
85
|
+
entity_records_service,
|
|
86
|
+
jwt_secret_key: str,
|
|
87
|
+
jwt_algorithm: str
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
Contact authentication middleware using EntityRecordsService.
|
|
91
|
+
|
|
92
|
+
This middleware validates contact JWT tokens and attaches contact data to request.state.
|
|
93
|
+
|
|
94
|
+
JWT Payload Structure:
|
|
95
|
+
{
|
|
96
|
+
"sub": "contact@example.com",
|
|
97
|
+
"cid": "contact_record_id", # KEY IDENTIFIER for contacts
|
|
98
|
+
"uuid": "unique-uuid",
|
|
99
|
+
"exp": 1234567890,
|
|
100
|
+
"iat": 1234567890
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
entity_records_service: EntityRecordsService for entity operations
|
|
105
|
+
jwt_secret_key: JWT secret key
|
|
106
|
+
jwt_algorithm: JWT algorithm (e.g., HS256)
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
FastAPI dependency function that validates contact authentication
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
async def get_auth(
|
|
113
|
+
request: Request,
|
|
114
|
+
token: HTTPAuthorizationCredentials = Depends(security)
|
|
115
|
+
):
|
|
116
|
+
jwt_functions = JWTFunctions(
|
|
117
|
+
secret_key=jwt_secret_key,
|
|
118
|
+
algorithm=jwt_algorithm
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
if not token or not token.credentials:
|
|
123
|
+
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
124
|
+
|
|
125
|
+
# Decode JWT and extract payload
|
|
126
|
+
payload = jwt_functions.get_data(token=token.credentials)
|
|
127
|
+
|
|
128
|
+
# Extract cid (required for contact authentication)
|
|
129
|
+
cid = payload.get("cid")
|
|
130
|
+
if not cid:
|
|
131
|
+
raise UnauthorizedError(detail="Invalid contact token")
|
|
132
|
+
|
|
133
|
+
# Extract app_id from path parameters
|
|
134
|
+
app_id = request.path_params.get("app_id")
|
|
135
|
+
if not app_id:
|
|
136
|
+
raise UnauthorizedError(detail="App ID is required")
|
|
137
|
+
|
|
138
|
+
logger.info(f"Contact auth: app_id={app_id}, cid={cid}")
|
|
139
|
+
|
|
140
|
+
# Get app configuration (apps collection is not an entity collection)
|
|
141
|
+
# We still need BaseRepository for this until apps get moved to a service
|
|
142
|
+
base_repo = BaseRepository(db=entity_records_service.repository.db)
|
|
143
|
+
app = await base_repo.get_by_attr("id", app_id, collection_name="apps")
|
|
144
|
+
if not app:
|
|
145
|
+
logger.error(f"App not found: {app_id}")
|
|
146
|
+
raise UnauthorizedError(detail="App not found")
|
|
147
|
+
|
|
148
|
+
# Extract contact entity ID from app metadata
|
|
149
|
+
metadata = app.get("metadata", {})
|
|
150
|
+
if not metadata:
|
|
151
|
+
raise UnauthorizedError(detail="App doesn't have support for contacts")
|
|
152
|
+
|
|
153
|
+
features_entities = metadata.get("features_entities", {})
|
|
154
|
+
if "CONTACT" not in features_entities:
|
|
155
|
+
raise UnauthorizedError(detail="App doesn't have support for contacts")
|
|
156
|
+
|
|
157
|
+
contact_config = features_entities.get("CONTACT", {})
|
|
158
|
+
contact_entity_id = contact_config.get("Contacts")
|
|
159
|
+
|
|
160
|
+
if not contact_entity_id:
|
|
161
|
+
raise UnauthorizedError(detail="Contact entity not configured")
|
|
162
|
+
|
|
163
|
+
logger.info(f"Contact entity ID: {contact_entity_id}")
|
|
164
|
+
|
|
165
|
+
# Get contact record using EntityRecordsService
|
|
166
|
+
try:
|
|
167
|
+
contact = await entity_records_service.get_record_by_id(
|
|
168
|
+
entity_id=contact_entity_id,
|
|
169
|
+
record_id=cid,
|
|
170
|
+
convert_ids=False # Keep ObjectIds for internal use
|
|
171
|
+
)
|
|
172
|
+
except NotFoundError:
|
|
173
|
+
logger.error(f"Authentication failed: contact with id {cid} not found")
|
|
174
|
+
raise UnauthorizedError(detail="Authentication failed")
|
|
175
|
+
|
|
176
|
+
logger.info(f"✅ Contact authenticated: {cid}")
|
|
177
|
+
|
|
178
|
+
# Attach contact to request state
|
|
179
|
+
request.state.contact = contact
|
|
180
|
+
request.state.contact_entity_id = contact_entity_id
|
|
181
|
+
|
|
182
|
+
return contact
|
|
183
|
+
|
|
184
|
+
except UnauthorizedError:
|
|
185
|
+
raise
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"Authentication error: {str(e)}", exc_info=True)
|
|
188
|
+
raise UnauthorizedError(detail="Authentication failed")
|
|
189
|
+
|
|
190
|
+
return get_auth
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def unified_auth_middleware(
|
|
194
|
+
jwt_secret_key: str,
|
|
195
|
+
jwt_algorithm: str,
|
|
196
|
+
get_user_auth_middleware: Callable,
|
|
197
|
+
get_contact_auth_middleware: Callable,
|
|
198
|
+
):
|
|
199
|
+
"""
|
|
200
|
+
Unified authentication middleware that intelligently routes to the correct
|
|
201
|
+
authentication flow based on JWT payload.
|
|
202
|
+
|
|
203
|
+
Detection Logic:
|
|
204
|
+
- If JWT contains "cid" field → Contact authentication
|
|
205
|
+
- Otherwise → User authentication
|
|
206
|
+
|
|
207
|
+
Benefits:
|
|
208
|
+
- Single entry point for all authentication
|
|
209
|
+
- JWT decoded only once for efficiency
|
|
210
|
+
- Transparent routing based on token type
|
|
211
|
+
- No code changes needed in route handlers
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
jwt_secret_key: JWT secret key
|
|
215
|
+
jwt_algorithm: JWT algorithm (e.g., HS256)
|
|
216
|
+
get_user_auth_middleware: User auth middleware callable
|
|
217
|
+
get_contact_auth_middleware: Contact auth middleware callable
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
FastAPI dependency that handles unified authentication
|
|
221
|
+
"""
|
|
222
|
+
async def get_auth(
|
|
223
|
+
request: Request,
|
|
224
|
+
token: HTTPAuthorizationCredentials = Depends(security)
|
|
225
|
+
):
|
|
226
|
+
if not token or not token.credentials:
|
|
227
|
+
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
228
|
+
|
|
229
|
+
# Decode JWT once to inspect payload
|
|
230
|
+
jwt_functions = JWTFunctions(
|
|
231
|
+
secret_key=jwt_secret_key,
|
|
232
|
+
algorithm=jwt_algorithm
|
|
233
|
+
)
|
|
234
|
+
payload = jwt_functions.get_data(token=token.credentials)
|
|
235
|
+
|
|
236
|
+
# Route based on payload content
|
|
237
|
+
cid = payload.get("cid")
|
|
238
|
+
if cid:
|
|
239
|
+
# Contact flow: JWT contains contact ID
|
|
240
|
+
return await get_contact_auth_middleware(request=request, token=token)
|
|
241
|
+
|
|
242
|
+
# User flow: Standard user authentication
|
|
243
|
+
return await get_user_auth_middleware(request=request, token=token)
|
|
244
|
+
|
|
245
|
+
return get_auth
|
|
@@ -12,6 +12,7 @@ class Users(BaseModel):
|
|
|
12
12
|
name = Column(String(100), nullable=False)
|
|
13
13
|
is_active = Column(Boolean, default=True)
|
|
14
14
|
last_login_at = Column(DateTime, nullable=True)
|
|
15
|
+
deleted_at = Column(DateTime, nullable=True) # Soft delete timestamp
|
|
15
16
|
|
|
16
17
|
# Relationships
|
|
17
18
|
roles = relationship(
|
|
@@ -4,7 +4,7 @@ from pydantic import BaseModel
|
|
|
4
4
|
import casbin
|
|
5
5
|
from casbin_sqlalchemy_adapter import Adapter
|
|
6
6
|
from casbin_redis_watcher import RedisWatcher, WatcherOptions,new_watcher
|
|
7
|
-
from sqlalchemy import and_, select
|
|
7
|
+
from sqlalchemy import and_, or_, select
|
|
8
8
|
from sqlalchemy.orm import Session, joinedload
|
|
9
9
|
from ..schema import CreatePermissionSchema
|
|
10
10
|
from ..models import (
|
|
@@ -65,6 +65,7 @@ class RBACService:
|
|
|
65
65
|
self.enforcer = casbin.Enforcer(
|
|
66
66
|
policy_path, adapter
|
|
67
67
|
)
|
|
68
|
+
self.enforcer.enable_auto_save(True)
|
|
68
69
|
# Load policies
|
|
69
70
|
self.enforcer.load_policy()
|
|
70
71
|
|
|
@@ -181,23 +182,41 @@ class RBACService:
|
|
|
181
182
|
except Exception as e:
|
|
182
183
|
raise e
|
|
183
184
|
|
|
184
|
-
|
|
185
|
+
def build_filter(self,cond: dict):
|
|
186
|
+
if "and" in cond:
|
|
187
|
+
return and_(*[self.build_filter(c) for c in cond["and"]])
|
|
188
|
+
elif "or" in cond:
|
|
189
|
+
return or_(*[self.build_filter(c) for c in cond["or"]])
|
|
190
|
+
else:
|
|
191
|
+
# Multiple simple field=value pairs in the same dict
|
|
192
|
+
return and_(*[
|
|
193
|
+
getattr(Permission, field) == value
|
|
194
|
+
for field, value in cond.items()
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
async def get_permissions_by_condition(self, condition: dict):
|
|
185
199
|
"""
|
|
186
|
-
Get permission(s) based on
|
|
187
|
-
|
|
200
|
+
Get permission(s) based on nested logical conditions.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
{
|
|
204
|
+
"and": [
|
|
205
|
+
{"entity_id": "123"},
|
|
206
|
+
{"or": [
|
|
207
|
+
{"user_id": "456"},
|
|
208
|
+
{"group_id": "789"}
|
|
209
|
+
]}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
188
212
|
"""
|
|
189
213
|
with self.db() as session:
|
|
190
214
|
try:
|
|
191
|
-
query = session.query(Permission)
|
|
192
|
-
if use_filter_by:
|
|
193
|
-
query = query.filter_by(**condition)
|
|
194
|
-
else:
|
|
195
|
-
filters = [getattr(Permission, k) == v for k, v in condition.items()]
|
|
196
|
-
query = query.filter(*filters)
|
|
215
|
+
query = session.query(Permission).filter(self.build_filter(condition))
|
|
197
216
|
return query.all()
|
|
198
217
|
except Exception as e:
|
|
199
218
|
raise e
|
|
200
|
-
|
|
219
|
+
|
|
201
220
|
async def delete_permission_by_uuids(self,permission_uuids:List[str]):
|
|
202
221
|
"""
|
|
203
222
|
Delete a permission by uuids
|
|
@@ -278,7 +297,85 @@ class RBACService:
|
|
|
278
297
|
return self.get_user_only_permissions(user_uuid)
|
|
279
298
|
except Exception as e:
|
|
280
299
|
raise e
|
|
281
|
-
|
|
300
|
+
|
|
301
|
+
def revoke_all_user_access(self, user_uuid: str) -> dict:
|
|
302
|
+
"""
|
|
303
|
+
Revoke all roles and permissions from a user in a single operation.
|
|
304
|
+
This is typically used when soft-deleting a user.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
user_uuid: The UUID of the user to revoke all access from
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
dict: Summary of revoked roles and permissions
|
|
311
|
+
"""
|
|
312
|
+
with self.db() as session:
|
|
313
|
+
try:
|
|
314
|
+
if not session.is_active:
|
|
315
|
+
session.begin()
|
|
316
|
+
|
|
317
|
+
# Get all user roles
|
|
318
|
+
user_roles = (
|
|
319
|
+
session.query(UserRole)
|
|
320
|
+
.options(joinedload(UserRole.role))
|
|
321
|
+
.filter(UserRole.user_uuid == user_uuid)
|
|
322
|
+
.all()
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
role_uuids = [ur.role.uuid for ur in user_roles] if user_roles else []
|
|
326
|
+
|
|
327
|
+
# Get all direct user permissions
|
|
328
|
+
user_permissions = (
|
|
329
|
+
session.query(UserPermission)
|
|
330
|
+
.options(joinedload(UserPermission.permission))
|
|
331
|
+
.filter(UserPermission.user_uuid == user_uuid)
|
|
332
|
+
.all()
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
permission_uuids = [up.permission.uuid for up in user_permissions] if user_permissions else []
|
|
336
|
+
|
|
337
|
+
# Revoke all roles
|
|
338
|
+
if role_uuids:
|
|
339
|
+
session.query(UserRole).filter(
|
|
340
|
+
UserRole.user_uuid == user_uuid,
|
|
341
|
+
UserRole.role_uuid.in_(role_uuids)
|
|
342
|
+
).delete(synchronize_session=False)
|
|
343
|
+
|
|
344
|
+
# Revoke all direct permissions and remove from Casbin
|
|
345
|
+
if permission_uuids:
|
|
346
|
+
permissions = session.query(Permission).filter(
|
|
347
|
+
Permission.uuid.in_(permission_uuids)
|
|
348
|
+
).all()
|
|
349
|
+
|
|
350
|
+
# Remove Casbin policies
|
|
351
|
+
policies = [
|
|
352
|
+
[f"user:{user_uuid}", permission.resource, permission.action, permission.module]
|
|
353
|
+
for permission in permissions
|
|
354
|
+
]
|
|
355
|
+
removed_policies = self.enforcer.remove_policies(policies)
|
|
356
|
+
if removed_policies:
|
|
357
|
+
self.enforcer.save_policy()
|
|
358
|
+
self.enforcer.load_policy()
|
|
359
|
+
|
|
360
|
+
# Delete from database
|
|
361
|
+
session.query(UserPermission).filter(
|
|
362
|
+
UserPermission.user_uuid == user_uuid,
|
|
363
|
+
UserPermission.permission_uuid.in_(permission_uuids)
|
|
364
|
+
).delete(synchronize_session=False)
|
|
365
|
+
|
|
366
|
+
session.commit()
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"user_uuid": user_uuid,
|
|
370
|
+
"roles_revoked": len(role_uuids),
|
|
371
|
+
"permissions_revoked": len(permission_uuids),
|
|
372
|
+
"role_uuids": role_uuids,
|
|
373
|
+
"permission_uuids": permission_uuids
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
except Exception as e:
|
|
377
|
+
raise e
|
|
378
|
+
|
|
282
379
|
def list_roles(self) -> Any:
|
|
283
380
|
"""
|
|
284
381
|
Get the list of all roles
|
|
@@ -392,7 +489,7 @@ class RBACService:
|
|
|
392
489
|
if not role:
|
|
393
490
|
raise NotFoundError(detail="Requested role does not exist")
|
|
394
491
|
|
|
395
|
-
return role
|
|
492
|
+
return role
|
|
396
493
|
|
|
397
494
|
def update_role_permissions(
|
|
398
495
|
self,
|
|
@@ -402,6 +499,7 @@ class RBACService:
|
|
|
402
499
|
description: Optional[str] = None,
|
|
403
500
|
) -> Any:
|
|
404
501
|
"""Update role permissions by replacing all existing permissions with new ones"""
|
|
502
|
+
|
|
405
503
|
with self.db() as session:
|
|
406
504
|
try:
|
|
407
505
|
if not session.is_active:
|
|
@@ -438,50 +536,46 @@ class RBACService:
|
|
|
438
536
|
role.description = description
|
|
439
537
|
|
|
440
538
|
if permissions is not None:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
[role_uuid, existing_permission.resource, existing_permission.action, existing_permission.module]
|
|
446
|
-
for existing_permission in existing_permissions
|
|
447
|
-
]
|
|
448
|
-
self.enforcer.remove_policies(remove_policies)
|
|
449
|
-
self.enforcer.save_policy()
|
|
450
|
-
|
|
451
|
-
# Delete existing role permissions
|
|
539
|
+
# Remove ALL existing policies for this role from Casbin
|
|
540
|
+
self.enforcer.remove_filtered_policy(0, str(role_uuid))
|
|
541
|
+
|
|
542
|
+
# Delete existing role permissions from database
|
|
452
543
|
session.query(RolePermission).filter(
|
|
453
544
|
RolePermission.role_uuid == role_uuid
|
|
454
545
|
).delete(synchronize_session=False)
|
|
455
546
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
found_permission_ids = {p.uuid for p in permissions_objs}
|
|
465
|
-
missing_permission_ids = set(permissions) - found_permission_ids
|
|
466
|
-
if missing_permission_ids:
|
|
467
|
-
raise NotFoundError(
|
|
468
|
-
detail=f"Permissions with UUIDs '{', '.join(missing_permission_ids)}' not found"
|
|
547
|
+
# Add new permissions if provided
|
|
548
|
+
if permissions:
|
|
549
|
+
# Fetch all permissions in a single query
|
|
550
|
+
permissions_objs = (
|
|
551
|
+
session.query(Permission)
|
|
552
|
+
.filter(Permission.uuid.in_(permissions))
|
|
553
|
+
.all()
|
|
469
554
|
)
|
|
470
555
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
556
|
+
found_permission_ids = {p.uuid for p in permissions_objs}
|
|
557
|
+
missing_permission_ids = set(permissions) - found_permission_ids
|
|
558
|
+
if missing_permission_ids:
|
|
559
|
+
raise NotFoundError(
|
|
560
|
+
detail=f"Permissions with UUIDs '{', '.join(missing_permission_ids)}' not found"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# Bulk insert role permissions
|
|
564
|
+
role_permissions = [
|
|
565
|
+
{"role_uuid": role_uuid, "permission_uuid": permission.uuid}
|
|
566
|
+
for permission in permissions_objs
|
|
567
|
+
]
|
|
568
|
+
session.bulk_insert_mappings(RolePermission, role_permissions)
|
|
569
|
+
|
|
570
|
+
# Add new Casbin policies
|
|
571
|
+
policies = [
|
|
572
|
+
[role_uuid, permission.resource, permission.action, permission.module]
|
|
573
|
+
for permission in permissions_objs
|
|
574
|
+
]
|
|
575
|
+
self.enforcer.add_policies(policies)
|
|
576
|
+
|
|
577
|
+
# Save all Casbin changes
|
|
578
|
+
# self.enforcer.save_policy()
|
|
485
579
|
|
|
486
580
|
session.commit()
|
|
487
581
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any, List, Optional, Callable
|
|
2
|
+
from sqlalchemy.orm import joinedload
|
|
3
|
+
from contextlib import AbstractContextManager
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
from abs_repository_core.repository.base_repository import BaseRepository
|
|
6
|
+
from abs_repository_core.schemas.base_schema import FindBase, FindUniqueValues
|
|
7
|
+
from abs_auth_rbac_core.models.permissions import Permission
|
|
8
|
+
|
|
9
|
+
class PermissionRepository(BaseRepository):
|
|
10
|
+
def __init__(self, db: Callable[..., Session]):
|
|
11
|
+
self.db = db
|
|
12
|
+
super().__init__(db, Permission)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any, List, Optional, Callable
|
|
2
|
+
from sqlalchemy.orm import joinedload
|
|
3
|
+
from contextlib import AbstractContextManager
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
from abs_repository_core.repository.base_repository import BaseRepository
|
|
6
|
+
from abs_auth_rbac_core.models.roles import Role
|
|
7
|
+
from abs_exception_core.exceptions import NotFoundError
|
|
8
|
+
from abs_repository_core.schemas import FilterSchema, FindBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RoleRepository(BaseRepository):
|
|
12
|
+
def __init__(self, db: Callable[..., Session]):
|
|
13
|
+
self.db = db
|
|
14
|
+
super().__init__(db, Role)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
from abs_repository_core.services.base_service import BaseService
|
|
3
|
+
from abs_repository_core.schemas.base_schema import FindBase, FindUniqueValues
|
|
4
|
+
from abs_auth_rbac_core.models.roles import Role
|
|
5
|
+
from abs_auth_rbac_core.repository.permission_repository import PermissionRepository
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PermissionService(BaseService):
|
|
10
|
+
def __init__(self, repository:PermissionRepository):
|
|
11
|
+
super().__init__(repository)
|
|
12
|
+
self.repository = repository
|
|
13
|
+
|
|
14
|
+
def list_permissions(self, schema: FindBase, eager: bool = True):
|
|
15
|
+
return self.repository.read_by_options(schema, eager)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
from abs_repository_core.services.base_service import BaseService
|
|
3
|
+
from abs_repository_core.schemas.base_schema import FindBase, FindUniqueValues
|
|
4
|
+
from abs_auth_rbac_core.models.roles import Role
|
|
5
|
+
from abs_auth_rbac_core.repository.role_repository import RoleRepository
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RoleService(BaseService):
|
|
10
|
+
def __init__(self, repository:RoleRepository):
|
|
11
|
+
self.repository = repository
|
|
12
|
+
super().__init__(repository)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def list_roles(self, schema: FindBase, eager: bool = True):
|
|
16
|
+
return self.repository.read_by_options(schema, eager)
|
|
17
|
+
|
|
18
|
+
|
|
@@ -153,6 +153,10 @@ class PermissionAction(str, Enum):
|
|
|
153
153
|
VIEW_IMPORT_DATA = "VIEW_IMPORT_DATA"
|
|
154
154
|
VIEW_WORKFLOW = "VIEW_WORKFLOW"
|
|
155
155
|
VIEW_ERROR_DATA = "VIEW_ERROR_DATA"
|
|
156
|
+
VIEW_ENTITY_RECORD = "VIEW_ENTITY_RECORD"
|
|
157
|
+
EDIT_ENTITY_RECORD = "EDIT_ENTITY_RECORD"
|
|
158
|
+
DELETE_ENTITY_RECORD = "DELETE_ENTITY_RECORD"
|
|
159
|
+
CREATE_ENTITY_RECORD = "CREATE_ENTITY_RECORD"
|
|
156
160
|
|
|
157
161
|
|
|
158
162
|
|
|
@@ -172,12 +176,17 @@ class PermissionModule(str, Enum):
|
|
|
172
176
|
ASL = "ASL"
|
|
173
177
|
EDS = "EDS"
|
|
174
178
|
WORKFORCE_AGENT = "WORKFORCE_AGENT"
|
|
175
|
-
|
|
179
|
+
ENTITY = "ENTITY"
|
|
180
|
+
ENTITY_VIEW = "ENTITY_VIEW"
|
|
181
|
+
ENTITY_RECORD = "ENTITY_RECORD"
|
|
182
|
+
CHATBOT = "CHATBOT"
|
|
176
183
|
|
|
177
184
|
|
|
178
185
|
class PermissionResource(str, Enum):
|
|
186
|
+
ENTITIES = "ENTITIES"
|
|
179
187
|
DASHBOARD = "DASHBOARDS"
|
|
180
188
|
WORKFORCE_AGENT = "WORKFORCE_AGENT"
|
|
189
|
+
CHATBOT = "CHATBOT"
|
|
181
190
|
EPAR = "EPAR"
|
|
182
191
|
EPARS = "EPARS"
|
|
183
192
|
OCFO = "OCFO"
|
|
@@ -262,6 +271,7 @@ class PermissionResource(str, Enum):
|
|
|
262
271
|
AUTOMATION_ACTION = "AUTOMATION_ACTION"
|
|
263
272
|
AUTOMATION_INPUT = "AUTOMATION_INPUT"
|
|
264
273
|
AUTOMATION_HISTORY = "AUTOMATION_HISTORY"
|
|
274
|
+
USER_PROFILE = "USER_PROFILE"
|
|
265
275
|
|
|
266
276
|
|
|
267
277
|
class PermissionData(NamedTuple):
|
|
@@ -273,6 +283,22 @@ class PermissionData(NamedTuple):
|
|
|
273
283
|
|
|
274
284
|
|
|
275
285
|
class PermissionConstants:
|
|
286
|
+
# User Profile Permissions
|
|
287
|
+
USER_PROFILE_VIEW = PermissionData(
|
|
288
|
+
name="View User Profile",
|
|
289
|
+
description="Permission to view user profile",
|
|
290
|
+
module=PermissionModule.USER_MANAGEMENT,
|
|
291
|
+
resource=PermissionResource.USER_PROFILE,
|
|
292
|
+
action=PermissionAction.VIEW,
|
|
293
|
+
)
|
|
294
|
+
USER_PROFILE_EDIT = PermissionData(
|
|
295
|
+
name="Edit User Profile",
|
|
296
|
+
description="Permission to edit user profile",
|
|
297
|
+
module=PermissionModule.USER_MANAGEMENT,
|
|
298
|
+
resource=PermissionResource.USER_PROFILE,
|
|
299
|
+
action=PermissionAction.EDIT,
|
|
300
|
+
)
|
|
301
|
+
|
|
276
302
|
# Automation Builder Permissions
|
|
277
303
|
AUTOMATION_HISTORY_VIEW = PermissionData(
|
|
278
304
|
name="View Automation History",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "abs-auth-rbac-core"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.13"
|
|
4
4
|
description = "RBAC and Auth core utilities including JWT token management."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "AutoBridgeSystems", email = "info@autobridgesystems.com"}
|
|
@@ -18,7 +18,9 @@ dependencies = [
|
|
|
18
18
|
"casbin-sqlalchemy-adapter (>=1.4.0,<2.0.0)",
|
|
19
19
|
"psycopg2-binary (>=2.9.10,<3.0.0)",
|
|
20
20
|
"casbin-redis-watcher (>=1.3.0,<2.0.0)",
|
|
21
|
-
"abs-utils (>=0.4.
|
|
21
|
+
"abs-utils (>=0.4.1,<0.5.0)",
|
|
22
|
+
"abs-repository-core (>=0.3.0,<0.4.0)",
|
|
23
|
+
"abs-nosql-repository-core (>=0.11.8,<0.12.0)"
|
|
22
24
|
]
|
|
23
25
|
|
|
24
26
|
[build-system]
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
from fastapi import Depends, Request
|
|
2
|
-
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
3
|
-
import logging
|
|
4
|
-
from typing import Callable, Any
|
|
5
|
-
|
|
6
|
-
from .jwt_functions import JWTFunctions
|
|
7
|
-
from .auth_functions import get_user_by_attribute
|
|
8
|
-
from abs_exception_core.exceptions import UnauthorizedError
|
|
9
|
-
|
|
10
|
-
security = HTTPBearer()
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Dependency acting like per-route middleware
|
|
15
|
-
def auth_middleware(
|
|
16
|
-
db_session: Callable[...,Any],
|
|
17
|
-
jwt_secret_key:str,
|
|
18
|
-
jwt_algorithm:str
|
|
19
|
-
):
|
|
20
|
-
"""
|
|
21
|
-
This middleware is used for authentication of the user.
|
|
22
|
-
Args:
|
|
23
|
-
db_session: Callable[...,Any]: Session of the SQLAlchemy database engine
|
|
24
|
-
jwt_secret_key: Secret key of the JWT for jwt functions
|
|
25
|
-
jwt_algorithm: Algorithm used for JWT
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
"""
|
|
29
|
-
async def get_auth(request: Request, token: HTTPAuthorizationCredentials = Depends(security)):
|
|
30
|
-
jwt_functions = JWTFunctions(secret_key=jwt_secret_key,algorithm=jwt_algorithm)
|
|
31
|
-
try:
|
|
32
|
-
if not token or not token.credentials:
|
|
33
|
-
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
34
|
-
|
|
35
|
-
payload = jwt_functions.get_data(token=token.credentials)
|
|
36
|
-
uuid = payload.get("uuid")
|
|
37
|
-
|
|
38
|
-
user = get_user_by_attribute(db_session=db_session,attribute="uuid", value=uuid)
|
|
39
|
-
|
|
40
|
-
if not user:
|
|
41
|
-
logger.error(f"Authentication failed: User with id {uuid} not found")
|
|
42
|
-
raise UnauthorizedError(detail="Authentication failed")
|
|
43
|
-
|
|
44
|
-
# Attach user to request state
|
|
45
|
-
request.state.user = user
|
|
46
|
-
return user
|
|
47
|
-
|
|
48
|
-
except Exception as e:
|
|
49
|
-
logger.error(f"Authentication error: {str(e)}", exc_info=True)
|
|
50
|
-
raise UnauthorizedError(detail="Authentication failed")
|
|
51
|
-
return get_auth
|
|
File without changes
|
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/auth/jwt_functions.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/__init__.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/base_model.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/gov_casbin_rule.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/permissions.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/rbac_model.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/role_permission.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/user_permission.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/models/user_role.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/schema/__init__.py
RENAMED
|
File without changes
|
{abs_auth_rbac_core-0.3.1 → abs_auth_rbac_core-0.3.13}/abs_auth_rbac_core/schema/permission.py
RENAMED
|
File without changes
|
|
File without changes
|