mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-11 03:44:59 +03:00
Single idempotent write boundary provision_organization coordinates Organization, OWNER membership or invitation, subscription/usage, system departments sales/support, audit and transactional outbox in one transaction under set_local_tenant. - New hub_platform.platform domain: PlatformOperator, hash-only PlatformToken, OrganizationProvisioning process record with state machine PENDING/IN_PROGRESS/WAITING_FOR_OWNER/COMPLETED/FAILED. - Platform API POST platform.custocrm.ru/api/v1/organizations: machine token auth + capability platform.organizations.provision, Idempotency-Key header, replay/conflict handling, no secrets in response. - OWNER invitation accept (accept_invitation + auth view) activates organization, subscription and first usage period; idempotent re-accept does not duplicate membership or period. - DB write boundary (tenancy/0005): platform INSERT/UPDATE on identity_organization and platform DML + tenant-isolation policy on identity_department/identity_employeeprofile; Organization.status ACTIVE/PENDING_OWNER (identity/0015). - System department codes centralised in identity/system_departments. Machine token without MFA is an owner-approved deviation from ADR-HUB-0031 section 9; human+MFA platform login belongs to track D. concurrent_p2p_calls derived from membership count is a separate C07 decision (PLAN-CUSTOCRM-0003 section 12). Gate: idempotent replay creates no second tenant; operator never becomes OWNER; provisioning creates no AI-agent/product/connection/demo data. Full backend regression 379 passed (one pre-existing unrelated SOCKS5 test); 19 C06 targeted tests passed. UI unchanged. Production migration/deploy, publication of production PlanVersion and bootstrap of the production platform token are not part of this commit.
42 lines
1.6 KiB
Python
42 lines
1.6 KiB
Python
from __future__ import annotations
|
|
|
|
from rest_framework.authentication import BaseAuthentication
|
|
|
|
from hub_platform.platform.models import PlatformOperator, PlatformToken
|
|
from hub_platform.platform.tokens import authenticate_token
|
|
|
|
|
|
class PlatformTokenAuthentication(BaseAuthentication):
|
|
"""Machine-to-machine auth via `Authorization: Token <opaque>`.
|
|
|
|
The platform surface is non-browser (ADR-HUB-0031 §4): there is no CORS and
|
|
session cookies are not used. On success, request.platform_operator and the
|
|
authenticating PlatformToken are attached for capability checks and audit.
|
|
"""
|
|
|
|
keyword = "Token"
|
|
|
|
def authenticate(self, request): # type: ignore[override]
|
|
header = request.headers.get("Authorization", "")
|
|
if not header.startswith(f"{self.keyword} "):
|
|
return None
|
|
raw_token = header[len(self.keyword) + 1 :].strip()
|
|
token = authenticate_token(raw_token)
|
|
if token is None:
|
|
return None
|
|
return (token.operator, token)
|
|
|
|
def authenticate_header(self, request): # type: ignore[override]
|
|
return self.keyword
|
|
|
|
def get_operator(self, request) -> PlatformOperator | None:
|
|
"""Helper for views needing the principal even when DRF has attached it
|
|
as request.user (here we keep it explicit on the request object)."""
|
|
return getattr(request, "platform_operator", None)
|
|
|
|
|
|
# DRF attaches the returned user/principal as request.user. The view layer reads
|
|
# request.user (a PlatformOperator) and the token via request.auth.
|
|
def get_platform_token(request) -> PlatformToken | None:
|
|
return getattr(request, "auth", None)
|