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:
151
src/database/settings.ts
Normal file
151
src/database/settings.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import prisma from "./prisma";
|
||||
|
||||
export interface EffectiveSetting {
|
||||
category: string;
|
||||
key: string;
|
||||
value: unknown;
|
||||
source: "user" | "tenant" | "customer" | "default";
|
||||
dataType: string;
|
||||
description: string | null;
|
||||
allowedValues: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve effective settings using the 3-level cascade:
|
||||
* definition default → customer override → tenant override → user override
|
||||
*
|
||||
* Only user-overridable settings are included.
|
||||
*/
|
||||
export async function getUserEffectiveSettings(
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
customerId: string | undefined,
|
||||
categories: string[]
|
||||
): Promise<Record<string, Record<string, EffectiveSetting>>> {
|
||||
// 1. Fetch all definitions for requested categories
|
||||
const definitions = await prisma.settingsDefinition.findMany({
|
||||
where: { category: { in: categories }, allow_user_override: true },
|
||||
orderBy: [{ category: "asc" }, { display_order: "asc" }],
|
||||
});
|
||||
|
||||
// 2. Fetch customer overrides
|
||||
const customerSettings = customerId
|
||||
? await prisma.customerSetting.findMany({
|
||||
where: {
|
||||
customerId,
|
||||
settingId: { in: definitions.map((d) => d.id) },
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
// 3. Fetch tenant overrides
|
||||
const tenantSettings = await prisma.tenantSetting.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
settingId: { in: definitions.map((d) => d.id) },
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Fetch user overrides
|
||||
const userSettings = await prisma.userSetting.findMany({
|
||||
where: {
|
||||
userId,
|
||||
settingId: { in: definitions.map((d) => d.id) },
|
||||
},
|
||||
});
|
||||
|
||||
// Build lookup maps
|
||||
const custMap = new Map(customerSettings.map((s) => [s.settingId, s]));
|
||||
const tenantMap = new Map(tenantSettings.map((s) => [s.settingId, s]));
|
||||
const userMap = new Map(userSettings.map((s) => [s.settingId, s]));
|
||||
|
||||
// 5. Resolve cascade
|
||||
const result: Record<string, Record<string, EffectiveSetting>> = {};
|
||||
|
||||
for (const def of definitions) {
|
||||
let value: unknown = def.defaultValue;
|
||||
let source: EffectiveSetting["source"] = "default";
|
||||
|
||||
const custSetting = custMap.get(def.id);
|
||||
if (custSetting) {
|
||||
value = custSetting.value;
|
||||
source = "customer";
|
||||
}
|
||||
|
||||
const tenantSetting = tenantMap.get(def.id);
|
||||
if (tenantSetting) {
|
||||
// Only override if customer didn't mark as mandatory
|
||||
if (!custSetting?.is_mandatory || custSetting.allow_tenant_override) {
|
||||
value = tenantSetting.value;
|
||||
source = "tenant";
|
||||
}
|
||||
}
|
||||
|
||||
const userSetting = userMap.get(def.id);
|
||||
if (userSetting) {
|
||||
// Only override if tenant didn't mark as mandatory
|
||||
const canUserOverride = tenantSetting ? tenantSetting.allow_user_override !== false : true;
|
||||
if (canUserOverride) {
|
||||
value = userSetting.value;
|
||||
source = "user";
|
||||
}
|
||||
}
|
||||
|
||||
if (!result[def.category]) result[def.category] = {};
|
||||
result[def.category][def.key] = {
|
||||
category: def.category,
|
||||
key: def.key,
|
||||
value,
|
||||
source,
|
||||
dataType: def.dataType,
|
||||
description: def.description,
|
||||
allowedValues: def.allowed_values,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch upsert user settings. Each entry is { category, key, value }.
|
||||
*/
|
||||
export async function batchUpsertUserSettings(
|
||||
userId: string,
|
||||
settings: Array<{ category: string; key: string; value: unknown }>
|
||||
) {
|
||||
// Look up setting definition IDs
|
||||
const keys = settings.map((s) => ({ category: s.category, key: s.key }));
|
||||
const definitions = await prisma.settingsDefinition.findMany({
|
||||
where: {
|
||||
OR: keys.map((k) => ({ category: k.category, key: k.key })),
|
||||
},
|
||||
select: { id: true, category: true, key: true },
|
||||
});
|
||||
|
||||
const defMap = new Map(definitions.map((d) => [`${d.category}:${d.key}`, d.id]));
|
||||
|
||||
// Build upserts
|
||||
const operations = settings
|
||||
.map((s) => {
|
||||
const settingId = defMap.get(`${s.category}:${s.key}`);
|
||||
if (!settingId) return null;
|
||||
return prisma.userSetting.upsert({
|
||||
where: { userId_settingId: { userId, settingId } },
|
||||
update: { value: s.value as never },
|
||||
create: { userId, settingId, value: s.value as never },
|
||||
});
|
||||
})
|
||||
.filter(Boolean) as ReturnType<typeof prisma.userSetting.upsert>[];
|
||||
|
||||
await prisma.$transaction(operations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings definitions for given categories.
|
||||
*/
|
||||
export async function getSettingsDefinitions(categories: string[]) {
|
||||
return prisma.settingsDefinition.findMany({
|
||||
where: { category: { in: categories } },
|
||||
orderBy: [{ category: "asc" }, { display_order: "asc" }],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user