Create access module package
This commit is contained in:
2
src/govoplan_access/backend/auth/__init__.py
Normal file
2
src/govoplan_access/backend/auth/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Authentication and principal helpers for platform access."""
|
||||
|
||||
27
src/govoplan_access/backend/auth/principals.py
Normal file
27
src/govoplan_access/backend/auth/principals.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from govoplan_access.backend.permissions.evaluator import scopes_grant
|
||||
|
||||
|
||||
AuthMethod = Literal["session", "api_key", "service_account"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Principal:
|
||||
account_id: str
|
||||
membership_id: str | None
|
||||
tenant_id: str | None
|
||||
scopes: frozenset[str]
|
||||
group_ids: frozenset[str]
|
||||
auth_method: AuthMethod
|
||||
api_key_id: str | None = None
|
||||
session_id: str | None = None
|
||||
service_account_id: str | None = None
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
|
||||
def has(self, required: str) -> bool:
|
||||
return scopes_grant(self.scopes, required)
|
||||
81
src/govoplan_access/backend/auth/roles.py
Normal file
81
src/govoplan_access/backend/auth/roles.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_access.backend.db.models import Account, Group, GroupMembership, Membership, Role, RoleBinding
|
||||
from govoplan_access.backend.permissions.evaluator import expand_scopes
|
||||
from govoplan_core.core.modules import PermissionDefinition, SubjectType
|
||||
|
||||
|
||||
def membership_group_ids(session: Session, *, tenant_id: str, membership_id: str) -> frozenset[str]:
|
||||
rows = (
|
||||
session.query(Group.id)
|
||||
.join(GroupMembership, GroupMembership.group_id == Group.id)
|
||||
.filter(
|
||||
GroupMembership.tenant_id == tenant_id,
|
||||
GroupMembership.membership_id == membership_id,
|
||||
Group.is_active.is_(True),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
return frozenset(row[0] for row in rows)
|
||||
|
||||
|
||||
def roles_for_subject(
|
||||
session: Session,
|
||||
*,
|
||||
subject_type: SubjectType,
|
||||
subject_id: str,
|
||||
tenant_id: str | None,
|
||||
) -> list[Role]:
|
||||
query = (
|
||||
session.query(Role)
|
||||
.join(RoleBinding, RoleBinding.role_id == Role.id)
|
||||
.filter(
|
||||
RoleBinding.subject_type == subject_type,
|
||||
RoleBinding.subject_id == subject_id,
|
||||
)
|
||||
)
|
||||
if tenant_id is None:
|
||||
query = query.filter(RoleBinding.tenant_id.is_(None), Role.tenant_id.is_(None))
|
||||
else:
|
||||
query = query.filter(RoleBinding.tenant_id == tenant_id, Role.tenant_id == tenant_id)
|
||||
return query.order_by(Role.name.asc()).all()
|
||||
|
||||
|
||||
def membership_roles(session: Session, membership: Membership) -> list[Role]:
|
||||
roles_by_id = {
|
||||
role.id: role
|
||||
for role in roles_for_subject(
|
||||
session,
|
||||
subject_type="membership",
|
||||
subject_id=membership.id,
|
||||
tenant_id=membership.tenant_id,
|
||||
)
|
||||
}
|
||||
for group_id in membership_group_ids(session, tenant_id=membership.tenant_id, membership_id=membership.id):
|
||||
for role in roles_for_subject(session, subject_type="group", subject_id=group_id, tenant_id=membership.tenant_id):
|
||||
roles_by_id[role.id] = role
|
||||
return list(roles_by_id.values())
|
||||
|
||||
|
||||
def account_system_roles(session: Session, account: Account) -> list[Role]:
|
||||
return roles_for_subject(session, subject_type="account", subject_id=account.id, tenant_id=None)
|
||||
|
||||
|
||||
def principal_scopes(
|
||||
session: Session,
|
||||
*,
|
||||
account: Account,
|
||||
membership: Membership | None,
|
||||
include_system: bool,
|
||||
catalog: dict[str, PermissionDefinition],
|
||||
) -> list[str]:
|
||||
scopes: set[str] = set()
|
||||
if membership is not None:
|
||||
for role in membership_roles(session, membership):
|
||||
scopes.update(role.permissions or [])
|
||||
if include_system:
|
||||
for role in account_system_roles(session, account):
|
||||
scopes.update(role.permissions or [])
|
||||
return expand_scopes(scopes, catalog=catalog)
|
||||
21
src/govoplan_access/backend/auth/tokens.py
Normal file
21
src/govoplan_access/backend/auth/tokens.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
|
||||
|
||||
DEFAULT_RANDOM_BYTES = 32
|
||||
|
||||
|
||||
def generate_secret(prefix: str, *, random_bytes: int = DEFAULT_RANDOM_BYTES) -> str:
|
||||
return prefix + secrets.token_urlsafe(random_bytes)
|
||||
|
||||
|
||||
def hash_secret(secret: str) -> str:
|
||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def verify_secret(secret: str, expected_hash: str) -> bool:
|
||||
return hmac.compare_digest(hash_secret(secret), expected_hash)
|
||||
|
||||
Reference in New Issue
Block a user