Skip to content

Scoped RBAC and Tenancy ​

Multi-tenant foundation for MyHoreca Pay: organizations, locations, quotas, and scoped role assignments.

Model ​

EntityPurpose
OrganizationCustomer tenant / business ownership scope; can have child organizations and can exist without any Outlet
Sub-organizationChild organization under a parent Organization; inherits parent rules and parent OrganizationAdmin access
LocationLegacy runtime entity for the product Outlet / restaurant within organization
OrganizationQuotaLimits per organization; legacy Syrve/iiko capability columns are deprecated and ignored
UserRoleAssignmentScoped 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 ​

RoleScope
MegaAdminPlatform — all permissions
MegaManagerPlatform support — no production write approve
OrganizationAdminOne organization plus inherited child organizations — organization, users, Outlets/legacy locations, connections, and rules management
AccountantOrganization / Outlet scope — full payment workflow
ViewerRead-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 OrganizationAdmin assignment 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
       - Outlet

Runtime implementation details:

  • Organization.ParentOrganizationId is nullable.
  • Root organizations have ParentOrganizationId = null.
  • Child organizations have ParentOrganizationId set to their parent organization.
  • Organization can exist without any Location rows; the absence of an Outlet must not be solved with fake/default Outlet assignment.
  • OrganizationAdmin inheritance is evaluated in ScopedAuthorizationService.GetUserScopeAsync.
  • Parent OrganizationAdmin access 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 parent id.

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/users lists and creates users without invitation links for platform support flows.
  • /api/platform/users/{targetUserId}/roles assigns 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 / MegaManager assignments are platform-scope only; OrganizationAdmin, Accountant, and Viewer require organization or Outlet scope.
  • PaymentOperator, LocationAdmin, Admin, and Operator are 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.
  • OrganizationAdmin does 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 403 when the resource is found but the user lacks scoped permission, or 404/null when 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/context returns available organizations, available Outlets/legacy locations, requiresSelection, and optional autoSelectedContext;
  • POST /api/auth/context validates 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 403 with explicit error code;
  • nullable Outlet is accepted only for org-wide scope or platform-wide users;
  • existing IScopedAuthorizationService checks 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, optional X-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.
  • UserRoleAssignment does not include ConnectionId.
  • syrveConnectionId is 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/:connectionId synchronize 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/connections when 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, MaxUsers
  • AllowBankStatementUpload

Legacy AllowTestPaymentWrite / AllowProductionPaymentWrite columns remain in the DB contract for compatibility but are deprecated and ignored by the Syrve/iiko runtime.

Ownership propagation ​

SourcePropagates to
BankStatementImportBankTransaction, CounterpartyMatch
BankTransactionPaymentAllocation
PaymentAllocationSyrvePaymentPosting
SyrveConnectionSyrveDocumentSnapshot, SyncRun, PaymentCandidate
SyrvePaymentPostingPaymentAnomaly, Notification

API ​

See API reference — Platform, Organization, Location sections.

Settings UI (phase 3) ​

  • /settings route accepts any of: legacy settings.manage, scoped organization.quotas.view, platform.settings.view.
  • Sidebar «Настройки» uses the same permission set via hasAnyPermission (legacy compatibility + scoped names).
  • Tab «Пользователи и роли» requires organization.users.manage (or legacy users.manage); lists assignments from GET /api/organizations/{id}/users, assign/remove via existing role endpoints; role picker uses GET /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.queue or 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.