import dotenv from 'dotenv'; import axios from 'axios'; // Load environment variables dotenv.config(); console.log('Environment variables loaded.'); // Configuration constants const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com'; const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER; const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD; const REALM_NAME = 'office-automation'; console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME }); // Client ID for Paperless const PAPERLESS_CLIENT_ID = process.env.PAPERLESS_CLIENT_ID || 'paperless'; // Helper function for API error handling const handleAxiosError = (error, operation, config, response) => { console.error(`Error during ${operation}:`); if (config) { console.error('Request:', { method: config.method, url: config.url, headers: config.headers, data: config.data, }); } if (error.response) { console.error('Response:', { status: error.response.status, data: error.response.data }); } else { console.error('Error Message:', error.message); } throw error; }; // Get Admin Token async function getAdminToken() { console.log('Getting admin token...'); try { const response = await axios.post( `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`, new URLSearchParams({ 'client_id': 'admin-cli', 'username': ADMIN_USERNAME, 'password': ADMIN_PASSWORD, 'grant_type': 'password' }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ); console.log('Admin token received.'); return response.data.access_token; } catch (error) { handleAxiosError(error, 'getting admin token'); } } // Function to get client information by clientId async function getClientByClientId(token, clientId) { console.log(`Getting client information for ${clientId}...`); try { const response = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, { headers: { 'Authorization': `Bearer ${token}` }, params: { clientId } }); if (response.data.length === 0) { console.log(`Client ${clientId} not found`); return null; } console.log(`Client ${clientId} found.`); return response.data[0]; } catch (error) { handleAxiosError(error, `getting client ${clientId}`); return null; } } // Check if client exists const checkClientExists = async (token, clientId) => !!await getClientByClientId(token, clientId); // Get client mappers by client ID async function getClientMappers(token, clientId) { console.log(`Getting client mappers for ${clientId}...`); const client = await getClientByClientId(token, clientId); if (!client) { console.log(`Client ${clientId} not found, no mappers to get.`); return []; } try { const response = await axios.get( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log(`Client mappers for ${clientId} retrieved.`); return response.data; } catch (error) { handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response); return []; } } // Get client scopes for a client async function getClientScopes(token, clientId) { console.log(`Getting client scopes for ${clientId}...`); const client = await getClientByClientId(token, clientId); if (!client) { console.log(`Client ${clientId} not found, no client scopes to get.`); return []; } try { const response = await axios.get( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`, { headers: { 'Authorization': `Bearer ${token}` } } ); console.log(`Client scopes for ${clientId} retrieved.`); return response.data; } catch (error) { handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response); return []; } } // Get a specific client scope by name async function getClientScope(token, scopeName) { console.log(`Getting client scope "${scopeName}"...`); try { const response = await axios.get( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`, { headers: { 'Authorization': `Bearer ${token}` } } ); const foundScope = response.data.find(scope => scope.name === scopeName); if (!foundScope) { console.log(`Client Scope "${scopeName}" not found`); return null; } console.log(`Client scope "${scopeName}" found:`, foundScope); return foundScope; } catch (error) { handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response); return null; } } // Add a default client scope to a client async function addDefaultClientScope(token, clientId, scopeName) { console.log(`Adding client scope "${scopeName}" as default for client "${clientId}"...`); const client = await getClientByClientId(token, clientId); const scope = await getClientScope(token, scopeName); if (!client || !scope) { console.log(`Client "${clientId}" or scope "${scopeName}" not found, cannot add as default scope.`); return; } try { await axios.put( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`, null, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); console.log(`Client scope "${scopeName}" added as default scope for client "${clientId}"`); } catch (error) { handleAxiosError(error, `adding client scope "${scopeName}" as default for client "${clientId}"`); } } // Create client and manage mappers async function createClient(token, clientId, clientName, redirectUris) { console.log(`Creating client "${clientId}"...`); let client = await getClientByClientId(token, clientId); if (!client) { const clientConfig = { clientId: clientId, name: clientName, enabled: true, protocol: "openid-connect", publicClient: false, authorizationServicesEnabled: false, serviceAccountsEnabled: false, standardFlowEnabled: true, implicitFlowEnabled: false, directAccessGrantsEnabled: true, redirectUris: redirectUris, webOrigins: ["+"], defaultClientScopes: ["roles"], optionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"], rootUrl: process.env.PAPERLESS_URL, baseUrl: process.env.PAPERLESS_URL, adminUrl: process.env.PAPERLESS_URL, }; try { const response = await axios.post( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, clientConfig, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); console.log(`Client "${clientId}" created successfully`); client = response.data; } catch (error) { handleAxiosError(error, `creating client: ${clientId}`); return; } } else { console.log(`Client "${clientId}" already exists, checking mappers and client secret`); } if (client) { try { await axios.put( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`, { ...client, secret: process.env[`KEYCLOAK_${clientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`] }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); console.log(`Set client secret for client: ${clientId}`); } catch (error) { handleAxiosError(error, `setting client secret for client: ${clientId}`, error.config, error.response); } const existingMappers = await getClientMappers(token, clientId); const requiredMappers = [ { name: "groups", protocol: "openid-connect", protocolMapper: "oidc-group-membership-mapper", config: { "full.path": "true", "id.token.claim": "true", "access.token.claim": "true", "userinfo.token.claim": "true", "claim.name": "groups" } }, { name: "realm roles", protocol: "openid-connect", protocolMapper: "oidc-usermodel-realm-role-mapper", config: { "id.token.claim": "true", "access.token.claim": "true", "userinfo.token.claim": "true", "claim.name": "roles", "jsonType.label": "String", "multivalued": "true" } } ]; for (const mapper of requiredMappers) { const existingMapper = existingMappers.find(m => m.name === mapper.name); try { if (existingMapper) { await axios.put( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`, { ...existingMapper, ...mapper }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); console.log(`Mapper "${mapper.name}" updated for client "${clientId}"`); } else { await axios.post( `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`, mapper, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } } ); console.log(`Mapper "${mapper.name}" created for client "${clientId}"`); } } catch (error) { handleAxiosError(error, `managing mapper "${mapper.name}" for client "${clientId}"`, error.config, error.response); } } await addDefaultClientScope(token, clientId, "openid"); await addDefaultClientScope(token, clientId, "profile"); await addDefaultClientScope(token, clientId, "groups-paperless"); } } // Create default groups async function createDefaultGroups(token) { console.log('Creating default groups...'); const groups = [ { name: "paperless-admins", path: "/paperless-admins", attributes: { "description": ["Paperless administrators"] } }, { name: "paperless-users", path: "/paperless-users", attributes: { "description": ["Paperless regular users"] } }, { name: "paperless-service", path: "/paperless-service", attributes: { "description": ["Paperless service user"] } } ]; for (const group of groups) { try { const existingGroups = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, { headers: { 'Authorization': `Bearer ${token}` }, params: { search: group.name, exact: true } // Added exact: true for precise matching }); if (existingGroups.data.length > 0) { console.log(`Group "${group.name}" already exists`); continue; } await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, group, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); console.log(`Group "${group.name}" created successfully`); } catch (error) { if (error.response?.status === 409) { console.log(`Group "${group.name}" already exists`); } else { handleAxiosError(error, `creating group: ${group.name}`); } } } } // Function to add users to groups async function addUsersToGroups(token) { console.log('Adding users to groups...'); const users = [ { username: "testadmin", groups: ["paperless-admins", "paperless-users"] }, { username: "testuser", groups: ["paperless-users"] }, { username: "testserviceuser", groups: ["paperless-service"] } ]; for (const user of users) { try { const existingUsers = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, { headers: { 'Authorization': `Bearer ${token}` }, params: { username: user.username, exact: true } // Added exact: true for precise matching }); if (existingUsers.data.length === 0) { console.log(`User "${user.username}" not found`); continue; } const userId = existingUsers.data[0].id; for (const groupName of user.groups) { const groupsResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, { headers: { 'Authorization': `Bearer ${token}` }, params: { search: groupName, exact: true } }); if (groupsResponse.data.length > 0) { const groupId = groupsResponse.data[0].id; try { await axios.put(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users/${userId}/groups/${groupId}`, {}, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); console.log(`User "${user.username}" added to group "${groupName}"`); } catch (error) { handleAxiosError(error, `adding user "${user.username}" to group "${groupName}"`, error.config, error.response); } } else { console.log(`Group "${groupName}" not found`); } } } catch (error) { handleAxiosError(error, `adding user "${user.username}" to groups`, error.config, error.response); } } } async function createGroupsPaperlessScope(token) { const scopeName = "groups-paperless"; const mapperName = "groups-mapper"; console.log(`Starting createGroupsPaperlessScope`); let clientScope = await getClientScope(token, scopeName); if (!clientScope) { try { console.log(`Creating client scope "${scopeName}"`); clientScope = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`, { "name": scopeName, "protocol": "openid-connect", "description": "Provides access to user group information for Paperless.", // Hinzugefügt: Beschreibung "attributes": {}, "consentScreenText": "Grant access to user groups in Paperless", "includeInTokenScope": true }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); console.log(`Client scope "${scopeName}" created successfully`); clientScope = response.data; } catch (error) { console.error(`Error creating client scope "${scopeName}":`, error); handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response); return; } } else { console.log(`Client scope "${scopeName}" exists, skipping creation`); } console.log("Client scope creation step finished"); if (clientScope) { console.log(`Check for mapper "${mapperName}" in scope "${scopeName}"`); try { const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`, { headers: { 'Authorization': `Bearer ${token}` } } ); if (!mappersResponse.data.find(m => m.name === mapperName)) { try { await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`, { "name": mapperName, "protocol": "openid-connect", "protocolMapper": "oidc-group-membership-mapper", "config": { "full.path": "false", "id.token.claim": "false", "access.token.claim": "true", "userinfo.token.claim": "true", "claim.name": "groups", "add.to.introspection": "false" } }, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); console.log(`Mapper "${mapperName}" created for client scope "${scopeName}"`); } catch (error) { console.error(`Error creating mapper "${mapperName}" for scope "${scopeName}":`, error); handleAxiosError(error, `creating mapper for ${scopeName}`, error.config, error.response); } } else { console.log(`Mapper "${mapperName}" exists in client scope "${scopeName}", skipping creation`); } } catch (error) { console.error("Error checking for mappers:", error); handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response); } } console.log("Finished createGroupsPaperlessScope"); } // Main function async function setupPaperlessRealm() { try { console.log('Starting Paperless Keycloak setup...'); const token = await getAdminToken(); // Create Paperless client await createClient(token, PAPERLESS_CLIENT_ID, PAPERLESS_CLIENT_ID, [`https://docs.mrx8086.com/*`]); // Create groups await createDefaultGroups(token); // Create client scope groups-paperless await createGroupsPaperlessScope(token); // Add users to groups await addUsersToGroups(token); // Read the configuration of the Office-Automation realm with Admin Token if (token) { console.log("Master Realm Admin Token:", token); try { const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, { headers: { 'Authorization': `Bearer ${token}` } }); console.log("Office Automation Realm Configuration:", realmConfig.data); } catch (error) { handleAxiosError(error, 'getting office realm configuration', error.config, error.response); } } else { console.error("Error getting Master Realm admin token"); } console.log('Paperless Keycloak setup completed successfully'); } catch (error) { console.error('Paperless Keycloak setup failed:', error); process.exit(1); } } // Execute the script setupPaperlessRealm();