feat: automate Keycloak session settings config on dashboard startup
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s
This commit is contained in:
145
server.js
145
server.js
@@ -282,6 +282,75 @@ function dockerLogs(container, tail = 160) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function execContainerCommand(containerName, cmd) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const bodyStr = JSON.stringify({
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Cmd: cmd
|
||||||
|
});
|
||||||
|
|
||||||
|
const createResult = await dockerRequest(`/containers/${encodeURIComponent(containerName)}/exec`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(bodyStr)
|
||||||
|
},
|
||||||
|
body: bodyStr
|
||||||
|
});
|
||||||
|
|
||||||
|
const execId = createResult?.Id;
|
||||||
|
if (!execId) {
|
||||||
|
reject(new Error(`Failed to create exec instance for container ${containerName}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startBody = JSON.stringify({ Detach: false, Tty: false });
|
||||||
|
const req = http.request({
|
||||||
|
socketPath: DOCKER_SOCKET,
|
||||||
|
path: `/exec/${execId}/start`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(startBody)
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
const chunks = [];
|
||||||
|
res.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
res.on('end', () => {
|
||||||
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||||
|
reject(new Error(`Docker exec start returned status ${res.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const raw = Buffer.concat(chunks);
|
||||||
|
let cleanText = '';
|
||||||
|
let offset = 0;
|
||||||
|
while (offset + 8 <= raw.length) {
|
||||||
|
const size = raw.readUInt32BE(offset + 4);
|
||||||
|
const start = offset + 8;
|
||||||
|
const end = start + size;
|
||||||
|
if (end > raw.length) break;
|
||||||
|
cleanText += raw.slice(start, end).toString('utf8');
|
||||||
|
offset = end;
|
||||||
|
}
|
||||||
|
if (!cleanText && raw.length > 0) {
|
||||||
|
cleanText = raw.toString('utf8');
|
||||||
|
}
|
||||||
|
resolve(cleanText.trim());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(startBody);
|
||||||
|
req.end();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getContainers() {
|
async function getContainers() {
|
||||||
const containers = await dockerRequest('/containers/json?all=0');
|
const containers = await dockerRequest('/containers/json?all=0');
|
||||||
return containers.map((container) => ({
|
return containers.map((container) => ({
|
||||||
@@ -1016,9 +1085,85 @@ const server = http.createServer(async (req, res) => {
|
|||||||
await serveStatic(req, res);
|
await serveStatic(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function syncKeycloakSessionSettings(retries = 12, delayMs = 15000) {
|
||||||
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
|
try {
|
||||||
|
console.log(`[Keycloak Sync] SSO session duration automation check (Attempt ${attempt}/${retries})...`);
|
||||||
|
|
||||||
|
const inspect = await dockerRequest('/containers/auth/json').catch(() => null);
|
||||||
|
if (!inspect) {
|
||||||
|
console.log('[Keycloak Sync] "auth" container not found or Docker socket inaccessible. Skipping settings automation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = inspect.State?.Status || inspect.state;
|
||||||
|
if (state !== 'running') {
|
||||||
|
console.log(`[Keycloak Sync] "auth" container is in status "${state}". Waiting for it to run...`);
|
||||||
|
if (attempt < retries) {
|
||||||
|
await new Promise((r) => setTimeout(r, delayMs));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = inspect.Config?.Env || [];
|
||||||
|
const adminUser = env.find(e => e.startsWith('KC_BOOTSTRAP_ADMIN_USERNAME='))?.split('=')[1]
|
||||||
|
|| env.find(e => e.startsWith('KEYCLOAK_ADMIN='))?.split('=')[1]
|
||||||
|
|| 'admin';
|
||||||
|
const adminPass = env.find(e => e.startsWith('KC_BOOTSTRAP_ADMIN_PASSWORD='))?.split('=')[1]
|
||||||
|
|| env.find(e => e.startsWith('KEYCLOAK_ADMIN_PASSWORD='))?.split('=')[1];
|
||||||
|
|
||||||
|
if (!adminPass) {
|
||||||
|
console.log('[Keycloak Sync] Could not extract admin password from container environment. Skipping settings automation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Keycloak Sync] Authenticating kcadm.sh inside Keycloak container...');
|
||||||
|
const loginCmd = [
|
||||||
|
'/opt/keycloak/bin/kcadm.sh',
|
||||||
|
'config',
|
||||||
|
'credentials',
|
||||||
|
'--server', 'http://localhost:8080',
|
||||||
|
'--realm', 'master',
|
||||||
|
'--user', adminUser,
|
||||||
|
'--password', adminPass
|
||||||
|
];
|
||||||
|
await execContainerCommand('auth', loginCmd);
|
||||||
|
|
||||||
|
console.log('[Keycloak Sync] Updating session settings in the "mischlabs" realm...');
|
||||||
|
const updateCmd = [
|
||||||
|
'/opt/keycloak/bin/kcadm.sh',
|
||||||
|
'update',
|
||||||
|
'realms/mischlabs',
|
||||||
|
'-s', 'ssoSessionIdleTimeout=2592000', // 30 days
|
||||||
|
'-s', 'ssoSessionMaxLifespan=7776000', // 90 days
|
||||||
|
'-s', 'ssoSessionIdleTimeoutRememberMe=2592000', // 30 days
|
||||||
|
'-s', 'ssoSessionMaxLifespanRememberMe=7776000', // 90 days
|
||||||
|
'-s', 'offlineSessionIdleTimeout=7776000', // 90 days
|
||||||
|
'-s', 'offlineSessionMaxLifespan=15552000', // 180 days
|
||||||
|
'-s', 'offlineSessionMaxLifespanEnabled=true'
|
||||||
|
];
|
||||||
|
const updateOutput = await execContainerCommand('auth', updateCmd);
|
||||||
|
console.log('[Keycloak Sync] Realm settings updated successfully:', updateOutput);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[Keycloak Sync] Attempt ${attempt} failed: ${error.message}`);
|
||||||
|
if (attempt < retries) {
|
||||||
|
console.log(`[Keycloak Sync] Retrying in ${delayMs / 1000} seconds...`);
|
||||||
|
await new Promise((r) => setTimeout(r, delayMs));
|
||||||
|
} else {
|
||||||
|
console.error('[Keycloak Sync] All attempts to automate Keycloak session settings failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`MischLabs dashboard listening on ${PORT}`);
|
console.log(`MischLabs dashboard listening on ${PORT}`);
|
||||||
cleanupManualWatchtowerContainers().catch((error) => {
|
cleanupManualWatchtowerContainers().catch((error) => {
|
||||||
console.warn(`Manual Watchtower cleanup failed: ${error.message}`);
|
console.warn(`Manual Watchtower cleanup failed: ${error.message}`);
|
||||||
});
|
});
|
||||||
|
syncKeycloakSessionSettings().catch((error) => {
|
||||||
|
console.warn(`Keycloak session synchronization failed: ${error.message}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user