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() {
|
||||
const containers = await dockerRequest('/containers/json?all=0');
|
||||
return containers.map((container) => ({
|
||||
@@ -1016,9 +1085,85 @@ const server = http.createServer(async (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, () => {
|
||||
console.log(`MischLabs dashboard listening on ${PORT}`);
|
||||
cleanupManualWatchtowerContainers().catch((error) => {
|
||||
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