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>
152 lines
4.4 KiB
TypeScript
152 lines
4.4 KiB
TypeScript
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" }],
|
|
});
|
|
}
|