abs-auth-rbac-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of abs-auth-rbac-core might be problematic. Click here for more details.
- abs_auth_rbac_core/__init__.py +0 -0
- abs_auth_rbac_core/auth/__init__.py +3 -0
- abs_auth_rbac_core/auth/auth_functions.py +31 -0
- abs_auth_rbac_core/auth/jwt_functions.py +134 -0
- abs_auth_rbac_core/auth/middleware.py +50 -0
- abs_auth_rbac_core/models/__init__.py +7 -0
- abs_auth_rbac_core/models/base_model.py +20 -0
- abs_auth_rbac_core/models/gov_casbin_rule.py +25 -0
- abs_auth_rbac_core/models/permissions.py +26 -0
- abs_auth_rbac_core/models/rbac_model.py +10 -0
- abs_auth_rbac_core/models/role_permission.py +12 -0
- abs_auth_rbac_core/models/roles.py +21 -0
- abs_auth_rbac_core/models/seeder/permission_seeder.py +101 -0
- abs_auth_rbac_core/models/user.py +27 -0
- abs_auth_rbac_core/models/user_role.py +20 -0
- abs_auth_rbac_core/rbac/__init__.py +2 -0
- abs_auth_rbac_core/rbac/database.py +52 -0
- abs_auth_rbac_core/rbac/decorator.py +48 -0
- abs_auth_rbac_core/rbac/policy.conf +14 -0
- abs_auth_rbac_core/rbac/service.py +688 -0
- abs_auth_rbac_core/util/__init__.py +0 -0
- abs_auth_rbac_core/util/permission_constants.py +1624 -0
- abs_auth_rbac_core-0.1.0.dist-info/METADATA +232 -0
- abs_auth_rbac_core-0.1.0.dist-info/RECORD +25 -0
- abs_auth_rbac_core-0.1.0.dist-info/WHEEL +4 -0
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Callable, Any
|
|
2
|
+
from ..models import Users
|
|
3
|
+
|
|
4
|
+
from abs_exception_core.exceptions import NotFoundError, ValidationError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_user_by_attribute(db_session: Callable[...,Any],attribute: str, value: str):
|
|
8
|
+
"""
|
|
9
|
+
Get a user by an attribute.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
attribute (str): The attribute to get the user by.
|
|
13
|
+
value (str): The value of the attribute.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
User: The user object if found, otherwise None.
|
|
17
|
+
"""
|
|
18
|
+
with db_session() as session:
|
|
19
|
+
try:
|
|
20
|
+
if not hasattr(Users, attribute):
|
|
21
|
+
raise ValidationError(detail=f"Attribute {attribute} does not exist on the User model")
|
|
22
|
+
|
|
23
|
+
user = session.query(Users).filter(getattr(Users, attribute) == value).first()
|
|
24
|
+
|
|
25
|
+
if not user:
|
|
26
|
+
raise NotFoundError(detail="User not found")
|
|
27
|
+
|
|
28
|
+
return user
|
|
29
|
+
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise e
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, UTC
|
|
2
|
+
from typing import Dict, Type, Callable,Any
|
|
3
|
+
|
|
4
|
+
import jwt
|
|
5
|
+
from fastapi import Depends
|
|
6
|
+
from fastapi.security import HTTPBearer
|
|
7
|
+
from passlib.context import CryptContext
|
|
8
|
+
from abs_exception_core.exceptions import UnauthorizedError,ValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# === JWT Setup ===
|
|
12
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
13
|
+
bearer_scheme = HTTPBearer()
|
|
14
|
+
|
|
15
|
+
# === Password Hashing ===
|
|
16
|
+
|
|
17
|
+
class JWTFunctions:
|
|
18
|
+
def __init__(self,secret_key: str,algorithm: str,expire_minutes: int=None):
|
|
19
|
+
"""
|
|
20
|
+
Args:
|
|
21
|
+
secret_key (str): The secret key for the JWT token.
|
|
22
|
+
algorithm (str): The algorithm for the JWT token.
|
|
23
|
+
expire_minutes (int): The expiration time for the JWT token in minutes.
|
|
24
|
+
"""
|
|
25
|
+
self.secret_key = secret_key
|
|
26
|
+
self.algorithm = algorithm
|
|
27
|
+
self.expire_minutes = expire_minutes
|
|
28
|
+
|
|
29
|
+
def verify_password(self,plain_password: str, hashed_password: str) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Verify a plain password against a hashed password using the password hashing context.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
plain_password (str): The plain password to verify.
|
|
35
|
+
hashed_password (str): The hashed password to verify against.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
bool: True if the password is verified, False otherwise.
|
|
39
|
+
"""
|
|
40
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
41
|
+
|
|
42
|
+
def get_password_hash(self,password: str) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Generate a hashed password using the password hashing context.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
password (str): The plain password to hash.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: The hashed password.
|
|
51
|
+
"""
|
|
52
|
+
return pwd_context.hash(password)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# === Token Dependencies ===
|
|
56
|
+
|
|
57
|
+
async def get_token(self,token=Depends(bearer_scheme)) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Get the token from the bearer scheme.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
token (str): The token to get.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
str: The token without the bearer prefix.
|
|
66
|
+
"""
|
|
67
|
+
return str(token.credentials)
|
|
68
|
+
|
|
69
|
+
# === JWT Token Creation & Decoding ===
|
|
70
|
+
|
|
71
|
+
def create_jwt_token(self,data: dict, expires_delta: timedelta=None) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Create a JWT token.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data (dict): The data to encode in the token.
|
|
77
|
+
expires_delta (timedelta): The expiration time for the token.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
str: The JWT token.
|
|
81
|
+
"""
|
|
82
|
+
payload = data.copy()
|
|
83
|
+
if expires_delta:
|
|
84
|
+
expire = datetime.now(UTC) + expires_delta
|
|
85
|
+
else:
|
|
86
|
+
expire = datetime.now(UTC) + timedelta(
|
|
87
|
+
minutes=self.expire_minutes
|
|
88
|
+
)
|
|
89
|
+
payload.update({"exp": expire})
|
|
90
|
+
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
91
|
+
|
|
92
|
+
def decode_jwt(self,token: str) -> dict:
|
|
93
|
+
"""
|
|
94
|
+
Decode a JWT token.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
token (str): The token to decode.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
dict: The decoded token.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
return jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
104
|
+
except jwt.ExpiredSignatureError:
|
|
105
|
+
raise UnauthorizedError(detail="Token has expired")
|
|
106
|
+
except jwt.InvalidTokenError:
|
|
107
|
+
raise ValidationError(detail="Invalid token")
|
|
108
|
+
|
|
109
|
+
def get_data(self,token: str = Depends(get_token)) -> Dict:
|
|
110
|
+
"""
|
|
111
|
+
Get the data from the JWT token.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
token (str): The token to get the data from.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Dict: The decoded token.
|
|
118
|
+
"""
|
|
119
|
+
return self.decode_jwt(token)
|
|
120
|
+
|
|
121
|
+
# === Token Generators ===
|
|
122
|
+
|
|
123
|
+
def create_access_token(self,data: Dict, expires_delta: timedelta=None) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Create an access token.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
data (Dict): The data to encode in the token.
|
|
129
|
+
expires_delta (timedelta): The expiration time for the token.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
str: The access token.
|
|
133
|
+
"""
|
|
134
|
+
return self.create_jwt_token(data=data, expires_delta=expires_delta)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from fastapi import Depends
|
|
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
|
+
Users: User table fo teh system
|
|
25
|
+
jwt_secret_key: Secret key of the JWT for jwt functions
|
|
26
|
+
jwt_algorithm: Algorithm used for JWT
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
"""
|
|
30
|
+
def get_auth(token: HTTPAuthorizationCredentials = Depends(security)):
|
|
31
|
+
jwt_functions = JWTFunctions(secret_key=jwt_secret_key,algorithm=jwt_algorithm)
|
|
32
|
+
try:
|
|
33
|
+
if not token or not token.credentials:
|
|
34
|
+
raise UnauthorizedError(detail="Invalid authentication credentials")
|
|
35
|
+
|
|
36
|
+
payload = jwt_functions.get_data(token=token.credentials)
|
|
37
|
+
uuid = payload.get("uuid")
|
|
38
|
+
|
|
39
|
+
user = get_user_by_attribute(db_session=db_session,attribute="uuid", value=uuid)
|
|
40
|
+
|
|
41
|
+
if not user:
|
|
42
|
+
logger.error(f"Authentication failed: User with id {uuid} not found")
|
|
43
|
+
raise UnauthorizedError(detail="Authentication failed")
|
|
44
|
+
|
|
45
|
+
return user # Attach user for use in route
|
|
46
|
+
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Authentication error: {str(e)}", exc_info=True)
|
|
49
|
+
raise UnauthorizedError(detail="Authentication failed")
|
|
50
|
+
return get_auth
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Column, DateTime, Integer, String
|
|
5
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
6
|
+
|
|
7
|
+
Base = declarative_base()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseModel(Base):
|
|
11
|
+
"""
|
|
12
|
+
Base model for all models
|
|
13
|
+
"""
|
|
14
|
+
__abstract__ = True
|
|
15
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
16
|
+
uuid = Column(String(36), unique=True, default=lambda: str(uuid.uuid4()))
|
|
17
|
+
created_at = Column(DateTime, default=lambda: datetime.now(UTC))
|
|
18
|
+
updated_at = Column(
|
|
19
|
+
DateTime, default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC)
|
|
20
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from casbin_sqlalchemy_adapter import CasbinRule as BaseCasbinRule
|
|
2
|
+
from sqlalchemy import Column, Integer, String
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GovCasbinRule(BaseCasbinRule):
|
|
6
|
+
__tablename__ = "gov_casbin_rule"
|
|
7
|
+
__mapper_args__ = {"polymorphic_identity": "gov_casbin_rule", "concrete": True}
|
|
8
|
+
|
|
9
|
+
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
10
|
+
ptype = Column(String(255))
|
|
11
|
+
v0 = Column(String(255))
|
|
12
|
+
v1 = Column(String(255))
|
|
13
|
+
v2 = Column(String(255))
|
|
14
|
+
v3 = Column(String(255))
|
|
15
|
+
v4 = Column(String(255))
|
|
16
|
+
v5 = Column(String(255))
|
|
17
|
+
|
|
18
|
+
def __init__(self, ptype=None, v0=None, v1=None, v2=None, v3=None, v4=None, v5=None):
|
|
19
|
+
self.ptype = ptype
|
|
20
|
+
self.v0 = v0
|
|
21
|
+
self.v1 = v1
|
|
22
|
+
self.v2 = v2
|
|
23
|
+
self.v3 = v3
|
|
24
|
+
self.v4 = v4
|
|
25
|
+
self.v5 = v5
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from sqlalchemy import Column, String
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
|
|
4
|
+
from abs_auth_rbac_core.models.rbac_model import RBACBaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Permission(RBACBaseModel):
|
|
8
|
+
"""Permission model representing system permissions"""
|
|
9
|
+
|
|
10
|
+
__tablename__ = "gov_permissions"
|
|
11
|
+
|
|
12
|
+
resource = Column(
|
|
13
|
+
String(100), index=True
|
|
14
|
+
) # The resource this permission applies to
|
|
15
|
+
|
|
16
|
+
action = Column(
|
|
17
|
+
String(50), index=True
|
|
18
|
+
) # The action allowed (e.g., read, write, delete)
|
|
19
|
+
|
|
20
|
+
module = Column(String(100), index=True)
|
|
21
|
+
# The module this permission applies to
|
|
22
|
+
|
|
23
|
+
# Relationships
|
|
24
|
+
roles = relationship(
|
|
25
|
+
"Role", secondary="gov_role_permissions", back_populates="permissions"
|
|
26
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from abs_auth_rbac_core.models.base_model import BaseModel
|
|
2
|
+
from sqlalchemy import Column, String
|
|
3
|
+
|
|
4
|
+
class RBACBaseModel(BaseModel):
|
|
5
|
+
"""Base model for RBAC entities with common fields"""
|
|
6
|
+
|
|
7
|
+
__abstract__ = True
|
|
8
|
+
|
|
9
|
+
name = Column(String(100), index=True, unique=True)
|
|
10
|
+
description = Column(String(500), nullable=True)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from sqlalchemy import Column, ForeignKey, String
|
|
2
|
+
|
|
3
|
+
from abs_auth_rbac_core.models.base_model import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RolePermission(BaseModel):
|
|
7
|
+
"""Association model between roles and permissions"""
|
|
8
|
+
|
|
9
|
+
__tablename__ = "gov_role_permissions"
|
|
10
|
+
|
|
11
|
+
role_uuid = Column(String(36), ForeignKey("gov_roles.uuid"), index=True)
|
|
12
|
+
permission_uuid = Column(String(36), ForeignKey("gov_permissions.uuid"), index=True)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from sqlalchemy.orm import relationship
|
|
2
|
+
|
|
3
|
+
from abs_auth_rbac_core.models.rbac_model import RBACBaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Role(RBACBaseModel):
|
|
7
|
+
"""Role model representing user roles in the system"""
|
|
8
|
+
__tablename__ = "gov_roles"
|
|
9
|
+
|
|
10
|
+
# Relationships
|
|
11
|
+
permissions = relationship(
|
|
12
|
+
"Permission", secondary="gov_role_permissions", back_populates="roles"
|
|
13
|
+
)
|
|
14
|
+
users = relationship(
|
|
15
|
+
"Users", secondary="gov_user_roles", back_populates="roles", overlaps="user_roles"
|
|
16
|
+
)
|
|
17
|
+
user_roles = relationship(
|
|
18
|
+
"UserRole", back_populates="role", cascade="all, delete-orphan", overlaps="users,roles"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
eagers = ["permissions", "users"]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
from sqlalchemy.orm import Session
|
|
3
|
+
from typing import List,Callable
|
|
4
|
+
|
|
5
|
+
from ...util.permission_constants import (
|
|
6
|
+
PermissionConstants
|
|
7
|
+
)
|
|
8
|
+
from ...models import ( Permission,Role,UserRole,Users)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def seed_permissions(db:Session,emails:List[str]=[]) -> None:
|
|
12
|
+
"""Seed permissions into the database"""
|
|
13
|
+
logger.info("Starting permission seeding...")
|
|
14
|
+
|
|
15
|
+
# Get all current permissions
|
|
16
|
+
current_permissions = {p.name: p for p in db.query(Permission).all()}
|
|
17
|
+
|
|
18
|
+
# Get all defined permissions from constants
|
|
19
|
+
defined_permissions = {
|
|
20
|
+
p.name: p for p in PermissionConstants.get_all_permissions()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
logger.info(f"Found {len(current_permissions)} existing permissions")
|
|
24
|
+
logger.info(f"Found {len(defined_permissions)} defined permissions")
|
|
25
|
+
|
|
26
|
+
# Update or create permissions
|
|
27
|
+
for name, permission_data in defined_permissions.items():
|
|
28
|
+
if name in current_permissions:
|
|
29
|
+
# Update existing permission
|
|
30
|
+
existing_permission = current_permissions[name]
|
|
31
|
+
existing_permission.description = permission_data.description
|
|
32
|
+
existing_permission.resource = permission_data.resource
|
|
33
|
+
existing_permission.action = permission_data.action
|
|
34
|
+
existing_permission.module = permission_data.module
|
|
35
|
+
logger.debug(f"Updated permission: {name}")
|
|
36
|
+
else:
|
|
37
|
+
# Create new permission
|
|
38
|
+
new_permission = Permission(
|
|
39
|
+
name=permission_data.name,
|
|
40
|
+
description=permission_data.description,
|
|
41
|
+
resource=permission_data.resource,
|
|
42
|
+
action=permission_data.action,
|
|
43
|
+
module=permission_data.module
|
|
44
|
+
)
|
|
45
|
+
db.add(new_permission)
|
|
46
|
+
logger.debug(f"Created new permission: {name}")
|
|
47
|
+
|
|
48
|
+
# Remove permissions that no longer exist in constants
|
|
49
|
+
for name in current_permissions:
|
|
50
|
+
if name not in defined_permissions:
|
|
51
|
+
db.delete(current_permissions[name])
|
|
52
|
+
logger.debug(f"Removed obsolete permission: {name}")
|
|
53
|
+
|
|
54
|
+
logger.success("Permission seeding completed successfully!")
|
|
55
|
+
|
|
56
|
+
# Create or get super admin role
|
|
57
|
+
super_admin_role = db.query(Role).filter(Role.name == "super_admin").first()
|
|
58
|
+
if not super_admin_role:
|
|
59
|
+
super_admin_role = Role(
|
|
60
|
+
name="super_admin",
|
|
61
|
+
description="Super Administrator with all permissions",
|
|
62
|
+
)
|
|
63
|
+
db.add(super_admin_role)
|
|
64
|
+
db.flush() # Get the role UUID
|
|
65
|
+
|
|
66
|
+
logger.info("Created super admin role.")
|
|
67
|
+
|
|
68
|
+
for email in emails:
|
|
69
|
+
user = db.query(Users).filter(Users.email == email).first()
|
|
70
|
+
if not user:
|
|
71
|
+
# Create new user
|
|
72
|
+
user = Users(
|
|
73
|
+
email=email,
|
|
74
|
+
name=email.split("@")[0], # Use part before @ as name
|
|
75
|
+
is_active=True,
|
|
76
|
+
)
|
|
77
|
+
db.add(user)
|
|
78
|
+
db.flush() # Get the user UUID
|
|
79
|
+
logger.info(f"Created new user: {email}")
|
|
80
|
+
|
|
81
|
+
# Check if user already has super admin role
|
|
82
|
+
existing_role = (
|
|
83
|
+
db.query(UserRole)
|
|
84
|
+
.filter(
|
|
85
|
+
UserRole.user_uuid == user.uuid,
|
|
86
|
+
UserRole.role_uuid == super_admin_role.uuid,
|
|
87
|
+
)
|
|
88
|
+
.first()
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not existing_role:
|
|
92
|
+
# Assign super admin role to user
|
|
93
|
+
user_role = UserRole(
|
|
94
|
+
user_uuid=user.uuid,
|
|
95
|
+
role_uuid=super_admin_role.uuid,
|
|
96
|
+
)
|
|
97
|
+
db.add(user_role)
|
|
98
|
+
logger.info(f"Assigned super admin role to user: {email}")
|
|
99
|
+
|
|
100
|
+
db.commit()
|
|
101
|
+
logger.success("Super admin seeding completed successfully!")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from sqlalchemy import Boolean, Column, DateTime, String, Integer
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
|
|
4
|
+
from abs_auth_rbac_core.models.base_model import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Users(BaseModel):
|
|
8
|
+
"""User model representing the user in the system"""
|
|
9
|
+
__tablename__ = "gov_users"
|
|
10
|
+
|
|
11
|
+
email = Column(String(255), unique=True, index=True, nullable=False)
|
|
12
|
+
name = Column(String(100), nullable=False)
|
|
13
|
+
is_active = Column(Boolean, default=True)
|
|
14
|
+
last_login_at = Column(DateTime, nullable=True)
|
|
15
|
+
|
|
16
|
+
# Relationships
|
|
17
|
+
roles = relationship(
|
|
18
|
+
"Role", secondary="gov_user_roles", back_populates="users", lazy="joined", overlaps="user_roles"
|
|
19
|
+
)
|
|
20
|
+
user_roles = relationship(
|
|
21
|
+
"UserRole",
|
|
22
|
+
back_populates="user",
|
|
23
|
+
cascade="all, delete-orphan",
|
|
24
|
+
overlaps="roles"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
eagers = ["roles"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from sqlalchemy import Column, ForeignKey, String
|
|
2
|
+
from sqlalchemy.orm import relationship
|
|
3
|
+
|
|
4
|
+
from abs_auth_rbac_core.models.base_model import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserRole(BaseModel):
|
|
8
|
+
"""Association table for User-Role relationship"""
|
|
9
|
+
__tablename__ = "gov_user_roles"
|
|
10
|
+
|
|
11
|
+
user_uuid = Column(
|
|
12
|
+
String(36), ForeignKey("gov_users.uuid", ondelete="CASCADE"), nullable=False
|
|
13
|
+
)
|
|
14
|
+
role_uuid = Column(
|
|
15
|
+
String(36), ForeignKey("gov_roles.uuid", ondelete="CASCADE"), nullable=False
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Corrected relationships
|
|
19
|
+
user = relationship("Users", back_populates="user_roles", overlaps="roles,users")
|
|
20
|
+
role = relationship("Role", back_populates="user_roles", overlaps="roles,users")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
2
|
+
from typing import Any, Generator
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import create_engine, orm
|
|
6
|
+
from sqlalchemy.orm import Session
|
|
7
|
+
from abs_repository_core.models import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Database:
|
|
11
|
+
def __init__(self, db_url: str) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Initialize the database engine and session factory
|
|
14
|
+
"""
|
|
15
|
+
self._engine = create_engine(
|
|
16
|
+
db_url,
|
|
17
|
+
echo=False,
|
|
18
|
+
echo_pool=False,
|
|
19
|
+
pool_pre_ping=True,
|
|
20
|
+
pool_recycle=3600,
|
|
21
|
+
query_cache_size=0,
|
|
22
|
+
)
|
|
23
|
+
self._session_factory = orm.scoped_session(
|
|
24
|
+
orm.sessionmaker(
|
|
25
|
+
autocommit=False,
|
|
26
|
+
autoflush=False,
|
|
27
|
+
bind=self._engine,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def create_database(self) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Create all the tables in the database
|
|
34
|
+
"""
|
|
35
|
+
BaseModel.metadata.create_all(self._engine)
|
|
36
|
+
|
|
37
|
+
@contextmanager
|
|
38
|
+
def session(self) -> Generator[Any, Any, AbstractContextManager[Session]]:
|
|
39
|
+
"""
|
|
40
|
+
Provides a database session for the request
|
|
41
|
+
"""
|
|
42
|
+
session: Session = self._session_factory()
|
|
43
|
+
try:
|
|
44
|
+
yield session
|
|
45
|
+
except Exception as e:
|
|
46
|
+
session.rollback()
|
|
47
|
+
import traceback
|
|
48
|
+
|
|
49
|
+
logger.error(f"Exception: {e}\n{traceback.format_exc()}")
|
|
50
|
+
raise e
|
|
51
|
+
finally:
|
|
52
|
+
session.close()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Dict, List, Union
|
|
3
|
+
|
|
4
|
+
from abs_exception_core.exceptions import PermissionDeniedError
|
|
5
|
+
from .service import RBACService
|
|
6
|
+
|
|
7
|
+
def rbac_require_permission(permissions: Union[str, List[str]]):
|
|
8
|
+
"""
|
|
9
|
+
Decorator to enforce all required permissions for a user.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
permissions (str | list[str]): One or more "resource:action" strings.
|
|
13
|
+
|
|
14
|
+
Raises:
|
|
15
|
+
PermissionDeniedError: If the user lacks any one of the required permissions.
|
|
16
|
+
"""
|
|
17
|
+
if isinstance(permissions, str):
|
|
18
|
+
permissions = [permissions]
|
|
19
|
+
|
|
20
|
+
def decorator(func):
|
|
21
|
+
@wraps(func)
|
|
22
|
+
async def wrapper(
|
|
23
|
+
*args, current_user: Dict,rbac_service:RBACService, **kwargs,
|
|
24
|
+
):
|
|
25
|
+
current_user_id = current_user.get("uuid")
|
|
26
|
+
if not current_user_id:
|
|
27
|
+
raise PermissionDeniedError(
|
|
28
|
+
detail="User not found (missing 'uuid')."
|
|
29
|
+
)
|
|
30
|
+
for perm in permissions:
|
|
31
|
+
try:
|
|
32
|
+
module, resource, action = perm.split(":")
|
|
33
|
+
except ValueError:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Invalid permission format: '{perm}'. Expected 'module:resource:action'."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
has_permission = rbac_service.check_permission(
|
|
39
|
+
user_uuid=current_user_id, resource=resource, action=action,module=module
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not has_permission:
|
|
43
|
+
raise PermissionDeniedError(
|
|
44
|
+
detail=f"Permission denied: {action} on {resource} in {module}"
|
|
45
|
+
)
|
|
46
|
+
return await func(*args, current_user=current_user,rbac_service=rbac_service, **kwargs)
|
|
47
|
+
return wrapper
|
|
48
|
+
return decorator
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[request_definition]
|
|
2
|
+
r = sub, obj, act, module
|
|
3
|
+
|
|
4
|
+
[policy_definition]
|
|
5
|
+
p = sub, obj, act, module
|
|
6
|
+
|
|
7
|
+
[role_definition]
|
|
8
|
+
g = _, _, _
|
|
9
|
+
|
|
10
|
+
[policy_effect]
|
|
11
|
+
e = some(where (p.eft == allow))
|
|
12
|
+
|
|
13
|
+
[matchers]
|
|
14
|
+
m = (g(r.sub, p.sub, r.module) && r.obj == p.obj && r.act == p.act && r.module == p.module) || r.sub == "super_admin"
|