ferrox-py-auth 1.0.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.
- ferrox_py_auth/__init__.py +1 -0
- ferrox_py_auth/controllers/auth_controller.py +52 -0
- ferrox_py_auth/models/user.py +16 -0
- ferrox_py_auth/security/rbac.py +26 -0
- ferrox_py_auth/services/auth_service.py +52 -0
- ferrox_py_auth/services/gdpr_service.py +31 -0
- ferrox_py_auth-1.0.0.dist-info/METADATA +51 -0
- ferrox_py_auth-1.0.0.dist-info/RECORD +9 -0
- ferrox_py_auth-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# init
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from fastapi import Request
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from ferrox_py.core.controllers import BaseController
|
|
4
|
+
from ferrox_py.core.container import Container
|
|
5
|
+
from ..services.auth_service import AuthService
|
|
6
|
+
from ..services.gdpr_service import GdprService
|
|
7
|
+
from ..security.rbac import require_roles
|
|
8
|
+
|
|
9
|
+
class LoginPayload(BaseModel):
|
|
10
|
+
email: str
|
|
11
|
+
password_hash: str
|
|
12
|
+
|
|
13
|
+
class SsoPayload(BaseModel):
|
|
14
|
+
provider: str
|
|
15
|
+
provider_id: str
|
|
16
|
+
email: str
|
|
17
|
+
|
|
18
|
+
class AuthController(BaseController):
|
|
19
|
+
def __init__(self, container: Container):
|
|
20
|
+
super().__init__(prefix="/auth", tags=["Auth & IAM"])
|
|
21
|
+
|
|
22
|
+
self.auth = AuthService(container.resolve("JwtService"))
|
|
23
|
+
self.gdpr = GdprService(self.auth)
|
|
24
|
+
|
|
25
|
+
@self.router.get("/config")
|
|
26
|
+
async def get_config():
|
|
27
|
+
return self.ok(self.auth.get_config(), "Auth settings retrieved")
|
|
28
|
+
|
|
29
|
+
@self.router.post("/register")
|
|
30
|
+
async def register(payload: LoginPayload):
|
|
31
|
+
user = await self.auth.register_local(payload.email, payload.password_hash)
|
|
32
|
+
return self.created(user.model_dump(), "User registered. Please check your email.")
|
|
33
|
+
|
|
34
|
+
@self.router.post("/sso")
|
|
35
|
+
async def sso_login(payload: SsoPayload):
|
|
36
|
+
token = await self.auth.login_sso(payload.provider, payload.provider_id, payload.email)
|
|
37
|
+
return self.ok({"token": token}, "SSO Login successful")
|
|
38
|
+
|
|
39
|
+
@self.router.get("/gdpr/export")
|
|
40
|
+
@require_roles("user", "admin")
|
|
41
|
+
async def export_my_data(request: Request):
|
|
42
|
+
# Extract user_id from the authenticated request state
|
|
43
|
+
user_id = request.state.user["sub"]
|
|
44
|
+
data = await self.gdpr.export_data(user_id)
|
|
45
|
+
return self.ok(data, "GDPR Export ready")
|
|
46
|
+
|
|
47
|
+
@self.router.delete("/gdpr/forget")
|
|
48
|
+
@require_roles("user", "admin")
|
|
49
|
+
async def delete_my_account(request: Request):
|
|
50
|
+
user_id = request.state.user["sub"]
|
|
51
|
+
success = await self.gdpr.forget_me(user_id)
|
|
52
|
+
return self.ok({"deleted": success}, "Account permanently deleted")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
class Identity(BaseModel):
|
|
6
|
+
provider: str # e.g., 'local', 'google', 'facebook', 'apple'
|
|
7
|
+
provider_id: str # ID from the external provider or hash of password
|
|
8
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
9
|
+
|
|
10
|
+
class User(BaseModel):
|
|
11
|
+
id: str
|
|
12
|
+
email: str
|
|
13
|
+
email_verified: bool = False
|
|
14
|
+
roles: List[str] = ["user"]
|
|
15
|
+
identities: List[Identity] = []
|
|
16
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import List
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from ferrox_py.core.errors import FerroxError
|
|
5
|
+
|
|
6
|
+
def require_roles(*roles: str):
|
|
7
|
+
"""
|
|
8
|
+
RBAC Decorator.
|
|
9
|
+
Extracts the user 'roles' claim from the JWT (attached to request.state.user)
|
|
10
|
+
and validates it against the required roles.
|
|
11
|
+
"""
|
|
12
|
+
def decorator(func):
|
|
13
|
+
@wraps(func)
|
|
14
|
+
async def wrapper(request: Request, *args, **kwargs):
|
|
15
|
+
user_data = getattr(request.state, "user", None)
|
|
16
|
+
if not user_data:
|
|
17
|
+
raise FerroxError("Unauthorized - No JWT token found", 401)
|
|
18
|
+
|
|
19
|
+
user_roles = user_data.get("roles", [])
|
|
20
|
+
|
|
21
|
+
if not any(role in user_roles for role in roles):
|
|
22
|
+
raise FerroxError(f"Forbidden - Requires one of roles: {roles}", 403)
|
|
23
|
+
|
|
24
|
+
return await func(request, *args, **kwargs)
|
|
25
|
+
return wrapper
|
|
26
|
+
return decorator
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from ferrox_py.core.provider import injectable
|
|
2
|
+
from ferrox_py.core.errors import FerroxError
|
|
3
|
+
from ferrox_py.security.jwt import JwtService
|
|
4
|
+
from ..models.user import User, Identity
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
@injectable()
|
|
8
|
+
class AuthService:
|
|
9
|
+
def __init__(self, jwt_service: JwtService):
|
|
10
|
+
self.jwt = jwt_service
|
|
11
|
+
self.enforce_sso_only = False # Feature flag configurable by the application
|
|
12
|
+
|
|
13
|
+
# Mock DB
|
|
14
|
+
self._users_db: Dict[str, User] = {}
|
|
15
|
+
|
|
16
|
+
def get_config(self) -> dict:
|
|
17
|
+
"""Returns auth settings for the frontend (e.g. to hide password fields)."""
|
|
18
|
+
return {"enforce_sso_only": self.enforce_sso_only}
|
|
19
|
+
|
|
20
|
+
async def register_local(self, email: str, password_hash: str) -> User:
|
|
21
|
+
if self.enforce_sso_only:
|
|
22
|
+
raise FerroxError("Local registration is disabled by SSO policy", 403)
|
|
23
|
+
|
|
24
|
+
if email in self._users_db:
|
|
25
|
+
raise FerroxError("Email already in use", 400)
|
|
26
|
+
|
|
27
|
+
user = User(
|
|
28
|
+
id=email,
|
|
29
|
+
email=email,
|
|
30
|
+
identities=[Identity(provider="local", provider_id=password_hash)]
|
|
31
|
+
)
|
|
32
|
+
self._users_db[email] = user
|
|
33
|
+
|
|
34
|
+
# Here we would trigger the MailerService for email_verified=True
|
|
35
|
+
print(f"AuthService: Emitting Email Confirmation for {email}")
|
|
36
|
+
return user
|
|
37
|
+
|
|
38
|
+
async def login_sso(self, provider: str, provider_id: str, email: str) -> str:
|
|
39
|
+
"""Handles SSO login or account linkage if user already exists."""
|
|
40
|
+
user = self._users_db.get(email)
|
|
41
|
+
|
|
42
|
+
if not user:
|
|
43
|
+
# Create new user, auto-verify email since it comes from trusted SSO
|
|
44
|
+
user = User(id=email, email=email, email_verified=True, identities=[])
|
|
45
|
+
self._users_db[email] = user
|
|
46
|
+
|
|
47
|
+
# Link identity if not present
|
|
48
|
+
if not any(i.provider == provider for i in user.identities):
|
|
49
|
+
user.identities.append(Identity(provider=provider, provider_id=provider_id))
|
|
50
|
+
|
|
51
|
+
# Issue JWT
|
|
52
|
+
return self.jwt.sign({"sub": user.id, "roles": user.roles})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ferrox_py.core.provider import injectable
|
|
2
|
+
from ferrox_py.core.errors import FerroxError
|
|
3
|
+
from .auth_service import AuthService
|
|
4
|
+
from typing import Dict
|
|
5
|
+
|
|
6
|
+
@injectable()
|
|
7
|
+
class GdprService:
|
|
8
|
+
def __init__(self, auth_service: AuthService):
|
|
9
|
+
self.auth = auth_service
|
|
10
|
+
|
|
11
|
+
async def export_data(self, user_id: str) -> Dict:
|
|
12
|
+
"""Returns all PII (Personally Identifiable Information) for GDPR export."""
|
|
13
|
+
user = self.auth._users_db.get(user_id)
|
|
14
|
+
if not user:
|
|
15
|
+
raise FerroxError("User not found", 404)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
"account": user.model_dump(),
|
|
19
|
+
"consent_logs": [],
|
|
20
|
+
"activity_logs": []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async def forget_me(self, user_id: str) -> bool:
|
|
24
|
+
"""Right to be forgotten: Hard deletes the user and all associated identities."""
|
|
25
|
+
if user_id in self.auth._users_db:
|
|
26
|
+
del self.auth._users_db[user_id]
|
|
27
|
+
# In a real scenario, this would emit an event to soft-delete or anonymize
|
|
28
|
+
# related records across the entire microservice ecosystem.
|
|
29
|
+
print(f"GdprService: User {user_id} completely deleted from system.")
|
|
30
|
+
return True
|
|
31
|
+
return False
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ferrox-py-auth
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: IAM, SSO, GDPR and RBAC suite for Ferrox ecosystem.
|
|
5
|
+
Author: AI-Autistic-Intelligence
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: ferrox-py>=1.0.0
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# Ferrox-Py-Auth
|
|
12
|
+
|
|
13
|
+
## 1. Overview (What does this do?)
|
|
14
|
+
The `ferrox-py-auth` module is a specialized extension for the Ferrox ecosystem that provides a comprehensive Identity & Access Management (IAM) suite. It delivers pre-built functionalities for managing user registrations, Single Sign-On (SSO), Role-Based Access Control (RBAC), and compliance with European data privacy laws (GDPR).
|
|
15
|
+
|
|
16
|
+
## 2. Philosophy (Why does it exist?)
|
|
17
|
+
Authentication and authorization are complex and highly sensitive domains. Re-implementing secure login flows, password hashing, SSO integration, and GDPR-compliant data deletion mechanisms for every new project is not only inefficient but heavily prone to critical security vulnerabilities. This module exists to provide a battle-tested, standardized "plug-and-play" identity solution that adheres to strict zero-trust principles.
|
|
18
|
+
|
|
19
|
+
## 3. Target Audience (Who is it for?)
|
|
20
|
+
This module is intended for backend engineers and security architects building Enterprise SaaS, financial platforms, or any consumer-facing application where data privacy (like GDPR compliance) and secure access controls are legal or operational requirements.
|
|
21
|
+
|
|
22
|
+
## 4. Architecture (How does it work?)
|
|
23
|
+
`ferrox-py-auth` plugs directly into the **7-Layer Onion Request Pipeline** of the core `ferrox-py` framework, specifically occupying Layers 3 (Threat Engine) and 4 (Auth Guards). It registers a set of domain Services (`AuthService`, `GdprService`) into the central IoC Container and exposes abstract Data Models (`User`, `UserIdentity`) that can be persisted via MongoDB or SQLAlchemy adapters provided by the `ferrox-py` Data Component.
|
|
24
|
+
|
|
25
|
+
## 5. Installation / Setup
|
|
26
|
+
This package requires the core `ferrox-py` framework to function. Install it via pip:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install ferrox-py-auth
|
|
30
|
+
```
|
|
31
|
+
Ensure you have configured a persistent database layer (either SQL or NoSQL) in your core application to store the user identities.
|
|
32
|
+
|
|
33
|
+
## 6. Quickstart (Usage)
|
|
34
|
+
```python
|
|
35
|
+
from ferrox_py.core.app import FerroxApp
|
|
36
|
+
from ferrox_py.core.container import Container
|
|
37
|
+
from ferrox_py_auth import AuthModule
|
|
38
|
+
|
|
39
|
+
# Initialize the IoC container
|
|
40
|
+
container = Container()
|
|
41
|
+
|
|
42
|
+
# The AuthModule automatically registers the AuthService, GdprService,
|
|
43
|
+
# and Auth Guards into the container and application lifecycle.
|
|
44
|
+
container.register_module(AuthModule)
|
|
45
|
+
|
|
46
|
+
app = FerroxApp(container)
|
|
47
|
+
app.start()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 7. Ecosystem Integration
|
|
51
|
+
This module relies heavily on the **Security** component of the core `ferrox-py` framework for generating and parsing PASETO/JWT tokens. It also serves as a foundational dependency for `ferrox-py-commerce`, as a validated User context is required before initiating billing or subscription processes.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ferrox_py_auth/__init__.py,sha256=4zALCCxfSyzOKNmB_Y-R1A0iUhPpCOrKbkJ1srJWyxo,7
|
|
2
|
+
ferrox_py_auth/controllers/auth_controller.py,sha256=bI7qU6moF2unIxwGQziwwgDJZM_sxb5yLnJ6nkizhvA,2132
|
|
3
|
+
ferrox_py_auth/models/user.py,sha256=b4Qma_zHFx7mO1RoaYPqaIYRa7QJy6sqL8QLr6TtgUA,554
|
|
4
|
+
ferrox_py_auth/security/rbac.py,sha256=DUUFswQUYhQ18TQHua1ltUPuGnpRgTXMxlZPqfMH-I8,948
|
|
5
|
+
ferrox_py_auth/services/auth_service.py,sha256=L7ajfKLjOw6EaavYOulcxNwPi56HntQt3zhEhULUvwY,2091
|
|
6
|
+
ferrox_py_auth/services/gdpr_service.py,sha256=sr2k5Y5HH8T88CI9FZOQmD6pZIJOSGgPgPS3-qzmxCQ,1201
|
|
7
|
+
ferrox_py_auth-1.0.0.dist-info/METADATA,sha256=clnWySXRPSWZpKQemQLR3Ddbkc5luU4Q1SoyY2Ze0xI,2968
|
|
8
|
+
ferrox_py_auth-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
ferrox_py_auth-1.0.0.dist-info/RECORD,,
|