feat: add YAML-first users nav access (#2188)
This commit is contained in:
@@ -327,6 +327,94 @@ async function readJsonIfPresent(relativePath, fallback = null) {
|
||||
}
|
||||
}
|
||||
|
||||
async function attachAccessControlEnv(deploy) {
|
||||
const byProfile = {};
|
||||
for (const [profile, laneConfig] of Object.entries(deploy?.lanes ?? {})) {
|
||||
if (!isRuntimeLane(profile)) continue;
|
||||
const accessControl = laneConfig?.accessControl;
|
||||
if (!accessControl || typeof accessControl !== "object" || Array.isArray(accessControl)) continue;
|
||||
const users = await readConfigRef(accessControl.usersRef, `${profile}.accessControl.usersRef`);
|
||||
const navProfiles = await readConfigRef(accessControl.navProfilesRef, `${profile}.accessControl.navProfilesRef`);
|
||||
const env = accessControlEnvFromConfig({ users, navProfiles });
|
||||
if (Object.keys(env).length > 0) byProfile[profile] = { "hwlab-cloud-api": env };
|
||||
}
|
||||
Object.defineProperty(deploy, "__accessControlEnvByProfile", { value: byProfile, enumerable: false, configurable: true });
|
||||
}
|
||||
|
||||
async function readConfigRef(ref, label) {
|
||||
assert.equal(typeof ref, "string", `${label} must be a file reference`);
|
||||
const [filePath, fragment = ""] = ref.split("#");
|
||||
assert.ok(filePath, `${label} must include a file path`);
|
||||
const root = await readJson(filePath);
|
||||
return configFragment(root, fragment || "", label);
|
||||
}
|
||||
|
||||
function configFragment(value, fragment, label) {
|
||||
const pathText = String(fragment ?? "").replace(/^\//u, "");
|
||||
if (!pathText) return value;
|
||||
return pathText.split(/[./]/u).filter(Boolean).reduce((current, segment) => {
|
||||
assert.ok(current && typeof current === "object" && Object.hasOwn(current, segment), `${label} fragment is missing ${segment}`);
|
||||
return current[segment];
|
||||
}, value);
|
||||
}
|
||||
|
||||
function accessControlEnvFromConfig({ users, navProfiles }) {
|
||||
assert.ok(Array.isArray(users), "accessControl usersRef must resolve to an array");
|
||||
assert.ok(Array.isArray(navProfiles), "accessControl navProfilesRef must resolve to an array");
|
||||
const env = {};
|
||||
const renderedUsers = users.map((user, index) => normalizeAccessControlUser(user, index, env));
|
||||
const renderedProfiles = navProfiles.map(normalizeNavProfile);
|
||||
env.HWLAB_ACCESS_CONTROL_USERS_JSON = JSON.stringify(renderedUsers);
|
||||
env.HWLAB_ACCESS_CONTROL_NAV_PROFILES_JSON = JSON.stringify(renderedProfiles);
|
||||
return env;
|
||||
}
|
||||
|
||||
function normalizeAccessControlUser(user, index, env) {
|
||||
assert.ok(user && typeof user === "object" && !Array.isArray(user), `accessControl user ${index} must be an object`);
|
||||
const passwordHashRef = user.passwordHashRef && typeof user.passwordHashRef === "object" && !Array.isArray(user.passwordHashRef) ? user.passwordHashRef : null;
|
||||
const passwordHashEnv = configString(user.passwordHashEnv) || configString(passwordHashRef?.env);
|
||||
const secretRef = configString(passwordHashRef?.secretRef) || secretRefFromParts(passwordHashRef);
|
||||
if (passwordHashEnv && secretRef) env[passwordHashEnv] = `secretRef:${secretRef}`;
|
||||
return {
|
||||
id: requiredAccessConfigString(user.id, `accessControl user ${index}.id`),
|
||||
username: requiredAccessConfigString(user.username, `accessControl user ${index}.username`),
|
||||
displayName: configString(user.displayName) || configString(user.username),
|
||||
email: configString(user.email) || null,
|
||||
role: configString(user.role) === "admin" ? "admin" : "user",
|
||||
status: configString(user.status) === "disabled" ? "disabled" : "active",
|
||||
navProfile: requiredAccessConfigString(user.navProfile, `accessControl user ${index}.navProfile`),
|
||||
passwordHashEnv: passwordHashEnv || null
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeNavProfile(profile, index) {
|
||||
assert.ok(profile && typeof profile === "object" && !Array.isArray(profile), `accessControl nav profile ${index} must be an object`);
|
||||
const allowedIds = Array.isArray(profile.allowedIds) ? profile.allowedIds.map(configString).filter(Boolean) : [];
|
||||
assert.ok(allowedIds.length > 0, `accessControl nav profile ${index}.allowedIds must not be empty`);
|
||||
return {
|
||||
id: requiredAccessConfigString(profile.id, `accessControl nav profile ${index}.id`),
|
||||
displayName: configString(profile.displayName) || configString(profile.id),
|
||||
allowedIds
|
||||
};
|
||||
}
|
||||
|
||||
function secretRefFromParts(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
const sourceRef = configString(value.sourceRef ?? value.secretName);
|
||||
const targetKey = configString(value.targetKey ?? value.key);
|
||||
return sourceRef && targetKey ? `${sourceRef}/${targetKey}` : "";
|
||||
}
|
||||
|
||||
function configString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function requiredAccessConfigString(value, label) {
|
||||
const text = configString(value);
|
||||
assert.ok(text, `${label} is required`);
|
||||
return text;
|
||||
}
|
||||
|
||||
async function gitValue(args) {
|
||||
const result = await execFileAsync("git", args, { cwd: repoRoot, timeout: 10000, maxBuffer: 1024 * 1024 });
|
||||
return result.stdout.trim();
|
||||
@@ -997,6 +1085,7 @@ function deployServicesForProfile(deploy, profile) {
|
||||
copy.env = mergeEnvMaps(
|
||||
copy.env,
|
||||
serviceDeclarationEnvForProfile(deploy, profile, service.serviceId),
|
||||
accessControlEnvForProfile(deploy, profile, service.serviceId),
|
||||
runtimeStoreEnvForProfile(deploy, profile, service.serviceId),
|
||||
workbenchRuntimeClientEnvForProfile(deploy, profile, service.serviceId),
|
||||
workbenchRuntimeFactsQueryEnvForProfile(deploy, profile, service.serviceId),
|
||||
@@ -1014,6 +1103,7 @@ function deployServicesForProfile(deploy, profile) {
|
||||
serviceId: override.serviceId,
|
||||
env: mergeEnvMaps(
|
||||
serviceDeclarationEnvForProfile(deploy, profile, override.serviceId),
|
||||
accessControlEnvForProfile(deploy, profile, override.serviceId),
|
||||
runtimeStoreEnvForProfile(deploy, profile, override.serviceId),
|
||||
workbenchRuntimeClientEnvForProfile(deploy, profile, override.serviceId),
|
||||
workbenchRuntimeFactsQueryEnvForProfile(deploy, profile, override.serviceId),
|
||||
@@ -1031,6 +1121,10 @@ function deployServicesForProfile(deploy, profile) {
|
||||
return services;
|
||||
}
|
||||
|
||||
function accessControlEnvForProfile(deploy, profile, serviceId) {
|
||||
return deploy?.__accessControlEnvByProfile?.[profile]?.[serviceId] ?? {};
|
||||
}
|
||||
|
||||
function serviceDeclarationEnvForProfile(deploy, profile, serviceId) {
|
||||
if (!isRuntimeLane(profile)) return {};
|
||||
const env = deploy?.lanes?.[profile]?.serviceDeclarations?.[serviceId]?.env;
|
||||
@@ -5498,6 +5592,7 @@ async function plannedFiles(args) {
|
||||
defaultV02RuntimeEndpoint,
|
||||
defaultSourceRepo
|
||||
});
|
||||
await attachAccessControlEnv(deploy);
|
||||
const source = await resolveSourceRevision(args.sourceRevision);
|
||||
source.imageTag = imageTagForSource(args, source);
|
||||
let artifactCatalog = await readJsonIfPresent(args.catalogPath, null);
|
||||
|
||||
Reference in New Issue
Block a user