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.

File without changes
@@ -0,0 +1,3 @@
1
+ from .jwt_functions import JWTFunctions
2
+
3
+ __all__ = ["JWTFunctions"]
@@ -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,7 @@
1
+ from .permissions import Permission
2
+ from .roles import Role
3
+ from .user_role import UserRole
4
+ from .user import Users
5
+ from .role_permission import RolePermission
6
+
7
+ __all__ = ["Permission", "Role", "UserRole", "Users","RolePermission"]
@@ -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,2 @@
1
+ from .decorator import rbac_require_permission
2
+ from .service import RBACService
@@ -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"