fix(workspace): authorize tenant members via Core membership
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a846b3e7f1
commit
d0c1d296b2
@ -1,10 +1,14 @@
|
|||||||
from fastapi import Depends
|
from fastapi import Depends, Request
|
||||||
|
import httpx
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.security import get_current_user
|
from app.core.security import STATE_ACCESS_TOKEN, get_current_user
|
||||||
from shared.exceptions import ForbiddenError
|
from shared.exceptions import ForbiddenError
|
||||||
|
from shared.tenant import STATE_TENANT_ID
|
||||||
|
|
||||||
_ADMINS = {"platform_admin", "tenant_owner", "tenant_admin"}
|
_ADMINS = {"platform_admin", "tenant_owner", "tenant_admin"}
|
||||||
|
_EDITORS = {"tenant_editor"}
|
||||||
|
_READ_ONLY = {"tenant_viewer"}
|
||||||
|
|
||||||
|
|
||||||
def user_has_permission(user, permission: str) -> bool:
|
def user_has_permission(user, permission: str) -> bool:
|
||||||
@ -17,12 +21,68 @@ def user_has_permission(user, permission: str) -> bool:
|
|||||||
return len(parts) >= 3 and f"{parts[0]}.{parts[1]}.manage" in roles
|
return len(parts) >= 3 and f"{parts[0]}.{parts[1]}.manage" in roles
|
||||||
|
|
||||||
|
|
||||||
|
def _roles_allow(roles: set[str], permission: str) -> bool:
|
||||||
|
if roles & _ADMINS or "workspace.*" in roles:
|
||||||
|
return True
|
||||||
|
if roles & _EDITORS:
|
||||||
|
return True
|
||||||
|
if roles & _READ_ONLY:
|
||||||
|
return permission.endswith(".read") or permission.endswith(".enabled")
|
||||||
|
if permission in roles:
|
||||||
|
return True
|
||||||
|
parts = permission.split(".")
|
||||||
|
return len(parts) >= 3 and f"{parts[0]}.{parts[1]}.manage" in roles
|
||||||
|
|
||||||
|
|
||||||
|
async def _core_membership_roles(request: Request) -> set[str]:
|
||||||
|
cached = getattr(request.state, "workspace_core_roles", None)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
token = getattr(request.state, STATE_ACCESS_TOKEN, None)
|
||||||
|
tenant_id = getattr(request.state, STATE_TENANT_ID, None)
|
||||||
|
if not token or not tenant_id:
|
||||||
|
request.state.workspace_core_roles = set()
|
||||||
|
return set()
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"{settings.core_service_url.rstrip('/')}/api/v1/me",
|
||||||
|
headers={"Authorization": f"Bearer {token}"},
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
request.state.workspace_core_roles = set()
|
||||||
|
return set()
|
||||||
|
if response.status_code != 200:
|
||||||
|
request.state.workspace_core_roles = set()
|
||||||
|
return set()
|
||||||
|
data = response.json()
|
||||||
|
roles: set[str] = set()
|
||||||
|
if data.get("platform_role"):
|
||||||
|
roles.add(str(data["platform_role"]))
|
||||||
|
for membership in data.get("memberships") or []:
|
||||||
|
if str(membership.get("tenant_id")) != str(tenant_id):
|
||||||
|
continue
|
||||||
|
if str(membership.get("status") or "active") not in {"active", "ACTIVE"}:
|
||||||
|
continue
|
||||||
|
if membership.get("role"):
|
||||||
|
roles.add(str(membership["role"]))
|
||||||
|
if membership.get("is_owner"):
|
||||||
|
roles.add("tenant_owner")
|
||||||
|
request.state.workspace_core_roles = roles
|
||||||
|
return roles
|
||||||
|
|
||||||
|
|
||||||
def require_permissions(*permissions: str):
|
def require_permissions(*permissions: str):
|
||||||
async def dependency(user=Depends(get_current_user)):
|
async def dependency(request: Request, user=Depends(get_current_user)):
|
||||||
if not settings.auth_required or any(user_has_permission(user, permission) for permission in permissions):
|
if not settings.auth_required:
|
||||||
|
return user
|
||||||
|
if any(user_has_permission(user, permission) for permission in permissions):
|
||||||
|
return user
|
||||||
|
core_roles = await _core_membership_roles(request)
|
||||||
|
if any(_roles_allow(core_roles, permission) for permission in permissions):
|
||||||
return user
|
return user
|
||||||
raise ForbiddenError(
|
raise ForbiddenError(
|
||||||
"Permission denied",
|
"این عملیات برای نقش فعلی فضای کاری مجاز نیست.",
|
||||||
error_code="permission_denied",
|
error_code="permission_denied",
|
||||||
details={"required": list(permissions)},
|
details={"required": list(permissions)},
|
||||||
)
|
)
|
||||||
@ -30,10 +90,13 @@ def require_permissions(*permissions: str):
|
|||||||
return dependency
|
return dependency
|
||||||
|
|
||||||
|
|
||||||
async def require_workspace_access(user=Depends(get_current_user)):
|
async def require_workspace_access(request: Request, user=Depends(get_current_user)):
|
||||||
if not settings.auth_required:
|
if not settings.auth_required:
|
||||||
return user
|
return user
|
||||||
roles = set(user.roles)
|
roles = set(user.roles)
|
||||||
if roles & _ADMINS or any(role.startswith("workspace.") for role in roles):
|
if roles & _ADMINS or any(role.startswith("workspace.") for role in roles):
|
||||||
return user
|
return user
|
||||||
raise ForbiddenError("Permission denied", error_code="permission_denied")
|
core_roles = await _core_membership_roles(request)
|
||||||
|
if core_roles & (_ADMINS | _EDITORS | _READ_ONLY):
|
||||||
|
return user
|
||||||
|
raise ForbiddenError("دسترسی به فضای کاری برای این سازمان مجاز نیست.", error_code="permission_denied")
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends, Request
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@ -9,6 +9,7 @@ from shared.exceptions import UnauthorizedError
|
|||||||
from shared.security import CurrentUser
|
from shared.security import CurrentUser
|
||||||
|
|
||||||
bearer = HTTPBearer(auto_error=False)
|
bearer = HTTPBearer(auto_error=False)
|
||||||
|
STATE_ACCESS_TOKEN = "workspace_access_token"
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
@ -26,9 +27,13 @@ def get_jwt_validator() -> JWTValidator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(credentials: HTTPAuthorizationCredentials | None = Depends(bearer)):
|
async def get_current_user(
|
||||||
|
request: Request,
|
||||||
|
credentials: HTTPAuthorizationCredentials | None = Depends(bearer),
|
||||||
|
):
|
||||||
if not settings.auth_required:
|
if not settings.auth_required:
|
||||||
return CurrentUser(user_id="test-user", username="test", roles=["tenant_admin"])
|
return CurrentUser(user_id="test-user", username="test", roles=["tenant_admin"])
|
||||||
if credentials is None or not credentials.credentials:
|
if credentials is None or not credentials.credentials:
|
||||||
raise UnauthorizedError("توکن احراز هویت ارائه نشده است")
|
raise UnauthorizedError("توکن احراز هویت ارائه نشده است")
|
||||||
|
setattr(request.state, STATE_ACCESS_TOKEN, credentials.credentials)
|
||||||
return await get_jwt_validator().validate(credentials.credentials)
|
return await get_jwt_validator().validate(credentials.credentials)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user