From 559348f6bc313f9a4250f7695e4ba0f6d602b256 Mon Sep 17 00:00:00 2001 From: Ronni Baslund Date: Sun, 31 May 2026 17:20:36 +0200 Subject: [PATCH] feat(portal): real Security & audit page (+ bundled Storage / per-tenant-roles WIP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security & audit (admin) - Audit log: real, tenant-scoped — widened GET /tenants/:slug/audit with q/action/outcome/actorEmail/since/before; UI gains search, outcome + time filters, action chips, cursor pagination, and client-side CSV export. - Security policy: new tenant.securityPolicy (mfaMode, session idle/absolute, allowedCountries, ipAllowlist) + PATCH /tenants/:slug/security-policy (membership-gated, audited). Editable, labelled by enforcement status. - MFA: live enrollment overview via GET /tenants/:slug/mfa-status (Authentik countAuthenticators per member). - SSO apps (Dezky as IdP): real Authentik OIDC provider + application CRUD, scoped to the tenant group. New AuthentikClient methods (provider/app/binding + flow/key/scope discovery), TenantSsoApp schema, TenantSsoService (rollback on partial failure; client secret never stored), GET/POST/DELETE /tenants/:slug/sso-apps. Validated end-to-end against live Authentik. - Deferred: shared-flow MFA/geo/session enforcement (global auth-flow blast radius) — to be done as its own reviewed change. Bundled in-progress work that shares the same files (kept together so the tree stays green): - Storage page: StorageService + GET /tenants/:slug/storage (OCIS-backed), storage.get proxy, storage.vue. - Per-tenant roles: User.tenantRoles + MeProfile.tenantRoles plumbing. --- .env.example | 6 + apps/portal/composables/useMe.ts | 43 +- apps/portal/pages/admin/security.vue | 576 +++++++++++++++--- apps/portal/pages/admin/storage.vue | 148 +++-- .../server/api/tenants/[slug]/audit.get.ts | 18 +- .../api/tenants/[slug]/mfa-status.get.ts | 18 + .../tenants/[slug]/security-policy.patch.ts | 20 + .../tenants/[slug]/sso-apps/[id].delete.ts | 20 + .../api/tenants/[slug]/sso-apps/index.get.ts | 17 + .../api/tenants/[slug]/sso-apps/index.post.ts | 21 + .../server/api/tenants/[slug]/storage.get.ts | 19 + apps/portal/types/workspace.ts | 34 ++ apps/portal/utils/bytes.ts | 21 + .../docker-compose/docker-compose.yml | 9 + .../src/integrations/authentik.client.ts | 126 ++++ .../src/integrations/ocis.client.ts | 141 ++++- .../src/schemas/tenant-sso-app.schema.ts | 40 ++ .../platform-api/src/schemas/tenant.schema.ts | 23 + .../platform-api/src/schemas/user.schema.ts | 38 +- .../src/tenants/dto/create-sso-app.dto.ts | 14 + .../tenants/dto/update-security-policy.dto.ts | 33 + .../src/tenants/storage.service.ts | 142 +++++ .../src/tenants/tenant-sso.service.ts | 163 +++++ .../src/tenants/tenants.controller.ts | 104 ++++ .../src/tenants/tenants.module.ts | 6 +- .../src/tenants/tenants.service.ts | 73 +++ .../platform-api/src/users/users.service.ts | 19 +- 27 files changed, 1744 insertions(+), 148 deletions(-) create mode 100644 apps/portal/server/api/tenants/[slug]/mfa-status.get.ts create mode 100644 apps/portal/server/api/tenants/[slug]/security-policy.patch.ts create mode 100644 apps/portal/server/api/tenants/[slug]/sso-apps/[id].delete.ts create mode 100644 apps/portal/server/api/tenants/[slug]/sso-apps/index.get.ts create mode 100644 apps/portal/server/api/tenants/[slug]/sso-apps/index.post.ts create mode 100644 apps/portal/server/api/tenants/[slug]/storage.get.ts create mode 100644 apps/portal/utils/bytes.ts create mode 100644 services/platform-api/src/schemas/tenant-sso-app.schema.ts create mode 100644 services/platform-api/src/tenants/dto/create-sso-app.dto.ts create mode 100644 services/platform-api/src/tenants/dto/update-security-policy.dto.ts create mode 100644 services/platform-api/src/tenants/storage.service.ts create mode 100644 services/platform-api/src/tenants/tenant-sso.service.ts diff --git a/.env.example b/.env.example index 83cd43e..35147fd 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,12 @@ STALWART_ADMIN_PASSWORD=changeme_use_openssl_rand # OCIS # ──────────────────────────────────────── OCIS_ADMIN_PASSWORD=changeme_use_openssl_rand +# Dedicated OCIS service user (Authentik) used by platform-api to read drive +# quotas for the Storage page via an OIDC password grant. Must exist in +# Authentik, have access to the OCIS application, and hold the OCIS admin role +# (required to list all drives). See docs/NEXT-STEPS.md. +OCIS_SVC_USERNAME=svc-platform-api +OCIS_SVC_PASSWORD=changeme_use_openssl_rand # ──────────────────────────────────────── # Collabora diff --git a/apps/portal/composables/useMe.ts b/apps/portal/composables/useMe.ts index 166358e..de0d838 100644 --- a/apps/portal/composables/useMe.ts +++ b/apps/portal/composables/useMe.ts @@ -9,6 +9,9 @@ interface MeProfile { email: string name: string role: string + // Per-tenant role overrides keyed by tenantId; absent keys fall back to + // `role`. Serialized from platform-api's User.tenantRoles Map. + tenantRoles?: Record active: boolean platformAdmin: boolean tenantIds: string[] @@ -65,11 +68,39 @@ export function useMe() { const partner = computed(() => profile.value?.partner ?? null) const isPartnerStaff = computed(() => !!profile.value?.partnerId) const isPlatformAdmin = computed(() => !!profile.value?.platformAdmin) - // Customer admin of their own workspace — gates access to the /admin surface. - // `role` is 'owner' | 'admin' | 'member' from platform-api (User.role). - const isTenantAdmin = computed( - () => profile.value?.role === 'owner' || profile.value?.role === 'admin', - ) - return { state, profile, partner, isPartnerStaff, isPlatformAdmin, isTenantAdmin, fetchMe } + const isAdminRole = (r: string | undefined) => r === 'owner' || r === 'admin' + + // Effective role for a specific tenant — mirrors platform-api roleForTenant(): + // a per-tenant entry wins, else the legacy global `role`, else 'member'. + function roleForTenant(tenantId: string): 'owner' | 'admin' | 'member' { + const p = profile.value + return p?.tenantRoles?.[tenantId] ?? (p?.role as 'owner' | 'admin' | 'member') ?? 'member' + } + function isTenantAdminOf(tenantId: string): boolean { + return isAdminRole(roleForTenant(tenantId)) + } + + // Gates the /admin surface: true if the user administers AT LEAST ONE of + // their tenants. Per-tenant enforcement of *which* workspace they may admin + // happens once a tenant is in context (backend membership + roleForTenant). + // For existing single-role data this is identical to the old global check. + const isTenantAdmin = computed(() => { + const p = profile.value + if (!p) return false + if (p.tenantIds.length) return p.tenantIds.some((t) => isTenantAdminOf(t)) + return isAdminRole(p.role) + }) + + return { + state, + profile, + partner, + isPartnerStaff, + isPlatformAdmin, + isTenantAdmin, + roleForTenant, + isTenantAdminOf, + fetchMe, + } } diff --git a/apps/portal/pages/admin/security.vue b/apps/portal/pages/admin/security.vue index 32e5410..a73b24d 100644 --- a/apps/portal/pages/admin/security.vue +++ b/apps/portal/pages/admin/security.vue @@ -1,42 +1,135 @@