Appearance
Scoped RBAC and Tenancy
Multi-tenant foundation for MyHoreca Pay: organizations, locations, quotas, and scoped role assignments.
Model
| Entity | Purpose |
|---|---|
Organization | Customer tenant / business ownership scope; can have child organizations and can exist without any Outlet |
Sub-organization | Child organization under a parent Organization; inherits parent rules and parent OrganizationAdmin access |
Location | Legacy runtime entity for the product Outlet / restaurant within organization |
OrganizationQuota | Limits per organization; legacy Syrve/iiko capability columns are deprecated and ignored |
UserRoleAssignment | Scoped link: User + Role + optional Organization + optional Location |
Legacy flat UserRole (UserId + RoleId) remains for backward compatibility; auth reads UserRoleAssignment first.
The product/UI term is Outlet. Runtime entities and existing routes still use Location naming until a dedicated rename task. New user-facing docs should not introduce Location as the product term.
Default tenancy
Migration and seeder create:
- Organization:
019edf00-0000-7000-8000-000000000001— Default Organization - Location:
019edf00-0000-7000-8000-000000000002— Default Location
Existing business records are backfilled to default org/location.
Roles
| Role | Scope |
|---|---|
| MegaAdmin | Platform — all permissions |
| MegaManager | Platform support — no production write approve |
| OrganizationAdmin | One organization plus inherited child organizations — organization, users, Outlets/legacy locations, connections, and rules management |
| Accountant | Organization / Outlet scope — full payment workflow |
| Viewer | Read-only |
MHPAY-51 MVP seed creates only MegaAdmin, MegaManager, OrganizationAdmin, Accountant, and Viewer.
PaymentOperator and LocationAdmin are future-only / legacy-compatible names. They are not MVP seed roles, receive no MVP permissions, and are hidden from assignable-role lists even if legacy rows remain in an existing database.
Legacy aliases Admin and Operator are not created by the MVP seeder. Admin remains recognized by compatibility helpers as a platform alias only for existing data.
Permissions
Canonical names in ScopedPermissionNames (Domain/Enums/ScopedRbacEnums.cs):
- Platform:
platform.organizations.*,platform.quotas.manage,platform.settings.view,platform.settings.manage, … - Organization:
organization.* - Location:
location.bank_statements.upload,location.payments.approve, …
Legacy PermissionNames map to scoped equivalents via PermissionCompatibility for JWT policies.
Authorization
IScopedAuthorizationService checks permission for (userId, permission, organizationId?, locationId?).
- MegaAdmin: allow all
- MegaManager: platform + org/location (not
platform.payments.production_write_approve_future) - OrganizationAdmin: assigned org + all its Outlets/legacy locations for management/read prerequisites; an org-wide parent
OrganizationAdminassignment also grants inherited org-wide access to child organizations - Accountant / Viewer: assigned location or org-wide assignment with
LocationId=null - PaymentOperator / LocationAdmin: no MVP permissions
API list endpoints use ScopeQueryExtensions.ApplyLocationScopeFilter.
Organization hierarchy
The implemented ownership shape is:
text
Platform
- Organization
- Sub-organization
- Outlet
- OutletRuntime implementation details:
Organization.ParentOrganizationIdis nullable.- Root organizations have
ParentOrganizationId = null. - Child organizations have
ParentOrganizationIdset to their parent organization. Organizationcan exist without anyLocationrows; the absence of an Outlet must not be solved with fake/default Outlet assignment.OrganizationAdmininheritance is evaluated inScopedAuthorizationService.GetUserScopeAsync.- Parent
OrganizationAdminaccess is represented in the effective scope by adding child organization ids and org-wide child access. The direct role assignment remains on the parent; full inherited-access audit detail is still a follow-up for audit-specific tasks. - Child organizations are not fully independent tenants for administrative control, matching rule inheritance, audit reasoning, or ownership reasoning.
- Counterparty matching uses inherited rules: child organizations evaluate their own location/org-level rules plus parent organization-level rules. Parent location-specific rules do not apply to child Outlets.
Current API additions:
GET /api/organizations/{id}— scoped organization details.PUT /api/organizations/{id}— scoped organization update for organization admins.POST /api/organizations/{id}/children— create a child organization under parentid.
The existing /api/platform/organizations endpoints remain platform-management endpoints for MegaAdmin / MegaManager.
Platform user management
MHPAY-52 adds platform-managed user administration for MegaAdmin / MegaManager.
- Access is guarded by
platform.users.manage; implementation must not rely on role-name-only checks. /api/platform/userslists and creates users without invitation links for platform support flows./api/platform/users/{targetUserId}/rolesassigns or removes direct scoped role assignments.- A single user can have different roles in different organizations and Outlets.
- Direct platform assignment can target platform scope, organization scope, or Outlet/location scope.
- Assignable roles are the MVP role set only:
MegaAdmin,MegaManager,OrganizationAdmin,Accountant,Viewer. MegaAdmin/MegaManagerassignments are platform-scope only;OrganizationAdmin,Accountant, andViewerrequire organization or Outlet scope.PaymentOperator,LocationAdmin,Admin, andOperatorare not assignable MVP roles even if legacy rows exist.- Mutations write audit entries with actor, target user, role, organization scope, optional Outlet/location scope, and assignment id where applicable.
OrganizationAdmindoes not see/platform/users; organization admins stay on organization-scoped user management under/api/organizations/{id}/users....
Tenant isolation hardening
Legacy endpoints were reviewed and hardened so tenant scoped resources cannot be read or mutated by ID across organizations/locations.
Current policy:
- list endpoints for imports, name matches, counterparty rules/matches, allocations, Syrve documents/sync runs/postings, anomalies, and dashboard counters are filtered by the current user's organization/location scope;
- detail endpoints for bank imports/transactions, allocations, Syrve connections/documents/postings/evidence, and anomalies check the target resource scope before returning data;
- mutation endpoints for duplicate decisions, manual counterparty match, counterparty rule creation, allocation preview/confirm/approve/cancel, payment candidates, Syrve connection operations, document refresh/sync, posting queue/approve/retry/reconcile/dry-run/write-target, and anomalies check scoped permission before write;
- cross-tenant access returns
403when the resource is found but the user lacks scoped permission, or404/nullwhen the read path deliberately conceals inaccessible resources; - dashboard summary counts only resources visible in the current user's scope;
- audit log is conservative: MegaAdmin/MegaManager can view platform-wide entries, while non-platform users see their own audit entries only, with optional organization/location filters applied inside their allowed scope.
Syrve/iiko reference and debt reads use the scoped default SyrveConnection when a user context exists. Background workers without an HTTP user keep system-level behavior.
Active organization/Outlet context (MHPAY-53)
The login flow supports active context selection for organization/Outlet scope:
GET /api/auth/contextreturns available organizations, available Outlets/legacy locations,requiresSelection, and optionalautoSelectedContext;POST /api/auth/contextvalidates selected context against effective scope and writes audit (ActiveContext);- single-org users with one safe context can be auto-selected without extra blocking UI;
- multi-org users must choose manually before entering operational routes;
- context switching is available after login from layout/header action.
Backend enforcement:
- backend never trusts frontend context blindly;
- selected organization/outlet is revalidated on
POST /api/auth/context; - cross-org and cross-outlet selections return
403with explicit error code; - nullable Outlet is accepted only for org-wide scope or platform-wide users;
- existing
IScopedAuthorizationServicechecks remain source of truth for data access.
Storage strategy in this iteration:
- no Redis/session persistence introduced (scope-safe MVP);
- frontend stores active context centrally and sends headers (
X-Organization-Id, optionalX-Location-Id); - design remains backend-compatible with future server-side session/Redis adoption without contract break.
Navigation and connection context rules are fixed in ADR: Navigation & Tenancy Information Architecture v1. The ADR keeps MVP RBAC scopes at Platform / Organization / Outlet and treats syrveConnectionId as navigation/connection context, not as a UserRoleAssignment scope.
Active Syrve connection context
Active Syrve connection context is frontend UI/navigation state for connection-scoped Syrve screens. It extends the active organization/Outlet context; it does not replace organization or Outlet tenant checks.
Rules:
- MVP RBAC scopes remain Platform / Organization / Outlet only.
UserRoleAssignmentdoes not includeConnectionId.syrveConnectionIdis persisted only in frontend active-context storage as optional UI/navigation state.- Legacy active-context keys remain organization/Outlet-only (
workspace.organizationId,workspace.locationId); no legacy Syrve connection key is created. - Direct routes under
/syrve/connections/:connectionIdsynchronize the selected connection into frontend context for navigation continuity. - Old global Syrve read routes redirect through remembered/default/first connection resolution, then fall back to
/syrve/connectionswhen no connection exists. - Backend authorization has not changed; every backend path must still validate organization/Outlet access and selected connection ownership.
- Payment workflow routes remain Outlet-scoped and outside
/syrve/connections/:id.
For the route ownership matrix and sidebar grouping, link to the ADR above rather than duplicating the full architecture here.
Quotas
IOrganizationQuotaService enforces supported quotas:
MaxLocations,MaxRmsConnections,MaxUsersAllowBankStatementUpload
Legacy AllowTestPaymentWrite / AllowProductionPaymentWrite columns remain in the DB contract for compatibility but are deprecated and ignored by the Syrve/iiko runtime.
Ownership propagation
| Source | Propagates to |
|---|---|
| BankStatementImport | BankTransaction, CounterpartyMatch |
| BankTransaction | PaymentAllocation |
| PaymentAllocation | SyrvePaymentPosting |
| SyrveConnection | SyrveDocumentSnapshot, SyncRun, PaymentCandidate |
| SyrvePaymentPosting | PaymentAnomaly, Notification |
API
See API reference — Platform, Organization, Location sections.
Settings UI (phase 3)
/settingsroute accepts any of: legacysettings.manage, scopedorganization.quotas.view,platform.settings.view.- Sidebar «Настройки» uses the same permission set via
hasAnyPermission(legacy compatibility + scoped names). - Tab «Пользователи и роли» requires
organization.users.manage(or legacyusers.manage); lists assignments fromGET /api/organizations/{id}/users, assign/remove via existing role endpoints; role picker usesGET /api/roles. - Quota UI may still expose deprecated payment capability columns for compatibility; changing them must not enable or disable registered runtime capabilities.
Payment write scope
Any payment posting requires:
- scoped permission (
location.payments.queueor the compatible organization permission); - explicit manual approval: every new posting starts in
ApprovalRequired; - idempotency and payable/amount/allocation/anomaly checks;
- server applicability, credentials, HTTP/RPC and method-specific preflight;
- technical retry/payload/response limits.
Registered runtime capability is always on and is not granted by RBAC alone. Deprecated DB capability columns are ignored.