chore: bootstrap gscMy on @gsc/web-kit + PAM/JIT request flow

Initial commit for gscMy carved out as its own repo (was tracked
loosely under the monorepo's web/ which is gitignored).

What this contains:
- Auth: next-auth v5 via @gsc/web-kit createAuth (Keycloak only,
  identity sourced from claims, no admin.users writes)
- Chrome: @gsc/web-kit AdminShell — replaces the legacy MyShell.
  Sidebar JSON config carried over and mapped to DbMenuItem.
- Middleware: createAuthMiddleware. Public: /access-denied,
  /auth/keycloak, /signed-out, /api/health, /api/pam/approve.
- RP-initiated signout at /api/auth/signout → Keycloak end_session →
  /signed-out (mirrors gscAdmin).
- Phosphor-iconned access-denied + signed-out landing pages.

PAM/JIT request flow (ported from gscAdmin's pre-strip git history):
- /access page (Active + Eligible tables, request modal with
  duration slider + justification + optional MFA)
- API: /api/pam/{eligible, active, audit, request, approve/:token,
  revoke/:id}
- src/lib/{authz, pam, pam-mail, pam-mfa}.ts — same files as
  gscAdmin had before the strip. PAM tables (admin.privilege_*)
  are shared with gscAdmin; gscMy uses the same Prisma model defs.
- Top-bar widget shows active grants with countdown + revoke.

Build/Deploy: Dockerfile (monorepo-root context), k8s manifests for
my.gosec.internal, self-signed TLS placeholder, DNS A record.
Keycloak gsc-my client extended to include my.gosec.internal/* in
redirect_uris + web_origins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Super User
2026-05-18 13:46:13 +02:00
commit be1c4fe5f9
96 changed files with 49849 additions and 0 deletions

188
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,188 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pgcrypto, uuid_ossp(map: "uuid-ossp", schema: "public")]
schemas = ["admin", "public"]
}
// ─── admin schema (shared with gscAdmin) ───────────────────────
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
gscsid String @unique @db.VarChar(64)
firstName String? @map("first_name") @db.VarChar(128)
lastName String? @map("last_name") @db.VarChar(128)
displayName String? @map("display_name") @db.VarChar(256)
email String? @db.VarChar(256)
phone String? @db.VarChar(32)
mobile String? @db.VarChar(32)
avatarUrl String? @map("avatar_url") @db.VarChar(512)
timezone String? @default("UTC") @db.VarChar(64)
locale String? @default("en") @db.VarChar(10)
status String? @default("active") @db.VarChar(32)
lastLoginAt DateTime? @map("last_login_at") @db.Timestamptz(6)
lastActivityAt DateTime? @map("last_activity_at") @db.Timestamptz(6)
metadata Json? @default("{}")
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
settings UserSetting[]
@@index([gscsid], map: "idx_users_gscsid")
@@map("users")
@@schema("admin")
}
model SettingsDefinition {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
category String @db.VarChar(64)
key String @db.VarChar(128)
dataType String @map("data_type") @db.VarChar(32)
defaultValue Json? @map("default_value")
min_value Decimal? @db.Decimal
max_value Decimal? @db.Decimal
allowed_values Json?
allow_customer_override Boolean? @default(true)
allow_tenant_override Boolean? @default(true)
allow_user_override Boolean? @default(true)
description String?
display_order Int? @default(0)
customerSettings CustomerSetting[]
tenantSettings TenantSetting[]
userSettings UserSetting[]
@@unique([category, key])
@@map("settings_definitions")
@@schema("admin")
}
model CustomerSetting {
customerId String @map("customer_id") @db.Uuid
settingId String @map("setting_id") @db.Uuid
value Json
is_mandatory Boolean? @default(false)
allow_tenant_override Boolean? @default(true)
updated_by String? @db.Uuid
updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
setting SettingsDefinition @relation(fields: [settingId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@id([customerId, settingId])
@@index([settingId, customerId], map: "idx_customer_settings_lookup")
@@map("customer_settings")
@@schema("admin")
}
model TenantSetting {
tenantId String @map("tenant_id") @db.Uuid
settingId String @map("setting_id") @db.Uuid
value Json
is_mandatory Boolean? @default(false)
allow_user_override Boolean? @default(true)
updated_by String? @db.Uuid
updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
setting SettingsDefinition @relation(fields: [settingId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@id([tenantId, settingId])
@@index([settingId, tenantId], map: "idx_tenant_settings_lookup")
@@map("tenant_settings")
@@schema("admin")
}
model UserSetting {
userId String @map("user_id") @db.Uuid
settingId String @map("setting_id") @db.Uuid
value Json
updatedAt DateTime? @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
setting SettingsDefinition @relation(fields: [settingId], references: [id], onDelete: Cascade, onUpdate: NoAction)
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction)
@@id([userId, settingId])
@@map("user_settings")
@@schema("admin")
}
// ─── public schema ─────────────────────────────────────────────
model UserActivityLog {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
userId String @map("user_id") @db.Uuid
tenantId String @map("tenant_id") @db.Uuid
action String @db.VarChar(64)
target String? @db.VarChar(256)
metadata Json? @default("{}")
ipAddress String? @map("ip_address") @db.VarChar(45)
userAgent String? @map("user_agent") @db.VarChar(512)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
@@index([userId, createdAt(sort: Desc)], map: "idx_user_activity_user")
@@index([tenantId, createdAt(sort: Desc)], map: "idx_user_activity_tenant")
@@map("user_activity_log")
@@schema("public")
}
// ============================================================================
// Privileged Access Management (JIT elevation) — shared with gscAdmin.
// Tables created by gscAdmin's `20260518_pam_init` migration; gscMy
// only needs the model definitions to write/read them.
// See gscAdmin/docs/pam-plan.md for the full design.
// ============================================================================
model PrivilegePolicy {
roleName String @id @map("role_name") @db.VarChar(128)
maxDurationHours Int @default(4) @map("max_duration_hours") @db.SmallInt
defaultDurationHours Int @default(1) @map("default_duration_hours") @db.SmallInt
approvalMode String @default("audit") @map("approval_mode") @db.VarChar(16)
approverEmail String? @map("approver_email") @db.VarChar(256)
requiresMfa Boolean @default(false) @map("requires_mfa")
eligibleGroup String @map("eligible_group") @db.VarChar(128)
description String?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
grants PrivilegeGrant[]
@@map("privilege_policies")
@@schema("admin")
}
model PrivilegeGrant {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
gscsid String @db.VarChar(64)
roleName String @map("role_name") @db.VarChar(128)
status String @default("pending") @db.VarChar(16)
approvalToken String? @unique @map("approval_token") @db.VarChar(64)
requestedAt DateTime @default(now()) @map("requested_at") @db.Timestamptz(6)
grantedAt DateTime? @map("granted_at") @db.Timestamptz(6)
expiresAt DateTime @map("expires_at") @db.Timestamptz(6)
grantedBy String? @map("granted_by") @db.VarChar(256)
justification String
mfaEvidence Json? @map("mfa_evidence")
revokedAt DateTime? @map("revoked_at") @db.Timestamptz(6)
revokedBy String? @map("revoked_by") @db.VarChar(64)
revokeReason String? @map("revoke_reason")
policy PrivilegePolicy @relation(fields: [roleName], references: [roleName], onUpdate: Cascade)
@@index([gscsid, roleName], map: "idx_priv_grants_active")
@@index([expiresAt], map: "idx_priv_grants_expires")
@@map("privilege_grants")
@@schema("admin")
}
model PrivilegeAudit {
id BigInt @id @default(autoincrement())
ts DateTime @default(now()) @db.Timestamptz(6)
event String @db.VarChar(32)
gscsid String @db.VarChar(64)
roleName String @map("role_name") @db.VarChar(128)
grantId String? @map("grant_id") @db.Uuid
actorGscsid String? @map("actor_gscsid") @db.VarChar(64)
actorEmail String? @map("actor_email") @db.VarChar(256)
detail Json?
@@index([gscsid, ts(sort: Desc)], map: "idx_priv_audit_user_ts")
@@map("privilege_audit")
@@schema("admin")
}