Эх сурвалжийг харах

feat: Integrate client mapper management into setup_realm.js

This commit integrates the client mapper management logic into the setup_realm.js script,
ensuring that client mappers are correctly created or updated during the realm setup process.
The old getClientMappers function was removed and all requests are handled with the new ensureClientMappers function.
mrx8086 11 сар өмнө
parent
commit
bc5ba498f0

+ 2 - 1
scripts/setup/keycloak/.env

@@ -5,4 +5,5 @@ NEXTCLOUD_CLIENT_ID=nextcloud
 PAPERLESS_CLIENT_ID=paperless
 NODERED_CLIENT_ID=nodered
 TESTADMIN_PASSWORD=TestAdminPwd
-TESTUSER_PASSWORD=TestUserPwd
+TESTUSER_PASSWORD=TestUserPwd
+KEYCLOAK_NEXTCLOUD_CLIENT_SECRET=D939xgzoxi58T2XZShdUPZP4gsI0kBOu

+ 215 - 0
scripts/setup/keycloak/check_client_mappers.js

@@ -0,0 +1,215 @@
+import dotenv from 'dotenv';
+import axios from 'axios';
+
+// Lade Umgebungsvariablen
+dotenv.config();
+
+// Konfigurationskonstanten
+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';
+
+// Client IDs aus Umgebungsvariablen
+const NEXTCLOUD_CLIENT_ID = process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud';
+const PAPERLESS_CLIENT_ID = process.env.PAPERLESS_CLIENT_ID || 'paperless';
+const NODERED_CLIENT_ID = process.env.NODERED_CLIENT_ID || 'nodered';
+
+
+// Hilfsfunktion für API-Fehlerbehandlung
+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;
+};
+
+// Admin Token abrufen
+async function getAdminToken() {
+    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'
+                }
+            }
+        );
+        return response.data.access_token;
+    } catch (error) {
+        handleAxiosError(error, 'getting admin token');
+    }
+}
+
+
+// Funktion um Client Infos abzufragen
+async function getClient(token, clientId) {
+    try {
+        const response = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                },
+                params: {
+                    clientId: clientId
+                }
+            }
+        );
+        if (response.data.length === 0) {
+            console.error(`Client ${clientId} not found`);
+            return null;
+        }
+
+        return response.data[0];
+    } catch (error) {
+        handleAxiosError(error, `getting client ${clientId}`);
+    }
+}
+// Funktion um Client Mapper abzufragen
+async function getClientMappers(token, clientId) {
+    try {
+        const client = await getClient(token, clientId);
+        if (!client) {
+            return [];
+        }
+        const response = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                }
+            }
+        );
+        return response.data;
+    } catch (error) {
+      handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
+        return [];
+    }
+}
+
+async function ensureClientMappers(token, clientId) {
+     const client = await getClient(token, clientId);
+        if (!client) {
+            return console.error(`Client ${clientId} not found, can't create mappers.`);
+        }
+
+        let 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) {
+                    // Update existierenden Mapper
+                    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 {
+                    // Erstelle neuen Mapper
+                   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);
+                 // Wir werfen den Fehler nicht weiter, damit andere Mapper noch verarbeitet werden können
+            }
+       }
+     // Get Mappers and log
+     existingMappers = await getClientMappers(token, clientId);
+     console.log(`Current Mappers for Client ${clientId}:`, existingMappers.map(mapper => ({name: mapper.name, id: mapper.id})));
+}
+
+
+
+// Hauptfunktion
+async function main() {
+    try {
+        console.log('Starting Client Mapper Check...');
+        const token = await getAdminToken();
+
+          // Clients erstellen
+        const clients = [
+             NEXTCLOUD_CLIENT_ID,
+             PAPERLESS_CLIENT_ID,
+            NODERED_CLIENT_ID
+        ];
+        for (const client of clients) {
+             await ensureClientMappers(token, client);
+        }
+
+        console.log('Client Mapper Check completed successfully');
+    } catch (error) {
+        console.error('Client Mapper Check failed:', error);
+        process.exit(1);
+    }
+}
+
+// Script ausführen
+main();

+ 300 - 66
scripts/setup/keycloak/setup_realm.js

@@ -1,4 +1,3 @@
-// setup_realm.js
 import dotenv from 'dotenv';
 import axios from 'axios';
 
@@ -11,10 +10,18 @@ const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
 const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
 const REALM_NAME = 'office-automation';
 
-// Client IDs aus Umgebungsvariablen
-const NEXTCLOUD_CLIENT_ID = process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud';
-const PAPERLESS_CLIENT_ID = process.env.PAPERLESS_CLIENT_ID || 'paperless';
-const NODERED_CLIENT_ID = process.env.NODERED_CLIENT_ID || 'nodered';
+// Client IDs und deren Konfiguration
+const CLIENTS = {
+    [process.env.NEXTCLOUD_CLIENT_ID || 'nextcloud']: {
+        redirectUris: ["https://cloud.mrx8086.com/*"]
+    },
+    [process.env.PAPERLESS_CLIENT_ID || 'paperless']: {
+        redirectUris: ["https://docs.mrx8086.com/*"]
+    },
+    [process.env.NODERED_CLIENT_ID || 'nodered']: {
+        redirectUris: ["https://automate.mrx8086.com/*"]
+    }
+};
 
 // Hilfsfunktion für API-Fehlerbehandlung
 const handleAxiosError = (error, operation) => {
@@ -72,8 +79,9 @@ async function checkRealmExists(token) {
     }
 }
 
-// Prüfen ob Client existiert
-async function checkClientExists(token, clientId) {
+
+// Funktion um Client Infos abzufragen
+async function getClient(token, clientId) {
     try {
         const response = await axios.get(
             `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
@@ -86,12 +94,46 @@ async function checkClientExists(token, clientId) {
                 }
             }
         );
-        return response.data.length > 0;
+        if (response.data.length === 0) {
+            console.error(`Client ${clientId} not found`);
+            return null;
+        }
+
+        return response.data[0];
     } catch (error) {
-        handleAxiosError(error, `checking client existence: ${clientId}`);
+        handleAxiosError(error, `getting client ${clientId}`);
     }
 }
 
+// Prüfen ob Client existiert
+async function checkClientExists(token, clientId) {
+    const client = await getClient(token, clientId);
+    return !!client;
+}
+
+// Funktion um Client Mapper abzufragen
+async function getClientMappers(token, clientId) {
+    try {
+        const client = await getClient(token, clientId);
+        if (!client) {
+            return [];
+        }
+
+        const response = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/mappers`,
+            {
+                headers: {
+                    'Authorization': `Bearer ${token}`
+                }
+            }
+        );
+        return response.data;
+    } catch (error) {
+        // handleAxiosError(error, `getting client mappers for ${clientId}`);
+        // wir ignorieren den Fehler hier, da wir diese nun immer setzen
+        return [];
+    }
+}
 // Realm erstellen
 async function createRealm(token) {
     const realmConfig = {
@@ -156,50 +198,136 @@ async function createRealm(token) {
 
 // Client erstellen
 async function createClient(token, clientId, clientName, redirectUris) {
-    const clientConfig = {
-        clientId: clientId,
-        name: clientName,
-        enabled: true,
-        protocol: "openid-connect",
-        publicClient: false,
-        authorizationServicesEnabled: true,
-        serviceAccountsEnabled: true,
-        standardFlowEnabled: true,
-        implicitFlowEnabled: false,
-        directAccessGrantsEnabled: true,
-        redirectUris: redirectUris,
-        webOrigins: ["+"],
-        defaultClientScopes: [
-            "email",
-            "profile",
-            "roles",
-            "web-origins"
-        ],
-        optionalClientScopes: [
-            "address",
-            "phone",
-            "offline_access",
-            "microprofile-jwt"
-        ]
-    };
+    let client;
+    const clientExists = await checkClientExists(token, clientId);
 
-    try {
-        await axios.post(
-            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
-            clientConfig,
+    if (!clientExists) {
+        const clientConfig = {
+            clientId: clientId,
+            name: clientName,
+            enabled: true,
+            protocol: "openid-connect",
+            publicClient: false,
+            authorizationServicesEnabled: true,
+            serviceAccountsEnabled: true,
+            standardFlowEnabled: true,
+            implicitFlowEnabled: false,
+            directAccessGrantsEnabled: true,
+            redirectUris: redirectUris,
+            webOrigins: ["+"],
+            defaultClientScopes: [
+                "roles"
+            ],
+            optionalClientScopes: [
+                "address",
+                "phone",
+                "offline_access",
+                "microprofile-jwt"
+            ]
+        };
+
+        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 {
+        client = await getClient(token, clientId);
+        console.log(`Client ${clientId} already exists, checking mappers`);
+    }
+
+    if (client) {
+        // Hole existierende Mapper
+        const existingMappers = await axios.get(
+            `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
             {
                 headers: {
-                    'Authorization': `Bearer ${token}`,
-                    'Content-Type': 'application/json'
+                    'Authorization': `Bearer ${token}`
                 }
             }
-        );
-        console.log(`Client ${clientId} created successfully`);
-    } catch (error) {
-        handleAxiosError(error, `creating client: ${clientId}`);
+        ).then(response => response.data)
+        .catch(() => []);
+
+        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) {
+                    // Update existierenden Mapper
+                    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 {
+                    // Erstelle neuen Mapper
+                    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) {
+                console.error(`Error managing mapper ${mapper.name} for client ${clientId}:`, error.message);
+                // Wir werfen den Fehler nicht weiter, damit andere Mapper noch verarbeitet werden können
+            }
+        }
     }
 }
 
+
 // Gruppen erstellen
 async function createDefaultGroups(token) {
     const groups = [
@@ -221,6 +349,24 @@ async function createDefaultGroups(token) {
 
     for (const group of groups) {
         try {
+            // Prüfen ob Gruppe existiert
+            const existingGroup = await axios.get(
+                `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`,
+                {
+                    headers: {
+                        'Authorization': `Bearer ${token}`
+                    },
+                    params: {
+                        search: group.name,
+                    }
+                }
+            );
+
+            if (existingGroup.data.length > 0) {
+                console.log(`Group ${group.name} already exists`);
+                continue;
+            }
+
             await axios.post(
                 `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`,
                 group,
@@ -242,6 +388,49 @@ async function createDefaultGroups(token) {
     }
 }
 
+
+async function createTestToken(token, username) {
+    try {
+        const client = await getClient(token, NEXTCLOUD_CLIENT_ID);
+        if (!client)
+            return null;
+        const response = await axios.post(
+            `${KEYCLOAK_URL}/realms/${REALM_NAME}/protocol/openid-connect/token`,
+            new URLSearchParams({
+                'client_id': NEXTCLOUD_CLIENT_ID,
+                'client_secret': process.env.KEYCLOAK_NEXTCLOUD_CLIENT_SECRET,
+                'username': username,
+                'password': process.env.TESTADMIN_PASSWORD || "initial123!",
+                'grant_type': 'password'
+            }),
+            {
+                headers: {
+                    'Content-Type': 'application/x-www-form-urlencoded'
+                }
+            }
+        );
+        return response.data.access_token;
+    } catch (error) {
+        handleAxiosError(error, `getting test token for ${username}`);
+    }
+}
+
+// Funktion zum Decodieren eines JWT-Tokens
+function decodeToken(token) {
+    try {
+        const base64Url = token.split('.')[1];
+        const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+        const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
+            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+        }).join(''));
+
+        return JSON.parse(jsonPayload);
+    } catch (error) {
+        console.error("Error decoding token:", error.message);
+        return null;
+    }
+}
+
 // Test-User erstellen
 async function createInitialUsers(token) {
     const users = [
@@ -315,45 +504,90 @@ async function createInitialUsers(token) {
     }
 }
 
+
 // Hauptfunktion
 async function setupRealm() {
     try {
         console.log('Starting Keycloak setup...');
         const token = await getAdminToken();
-        
+
         // Prüfe ob Realm existiert
         const realmExists = await checkRealmExists(token);
-        
+
         if (!realmExists) {
             console.log('Creating new realm...');
             await createRealm(token);
-            
-            // Clients erstellen
-            const clients = [
-                { id: NEXTCLOUD_CLIENT_ID, name: "Nextcloud", redirectUris: ["https://cloud.mrx8086.com/*"] },
-                { id: PAPERLESS_CLIENT_ID, name: "Paperless", redirectUris: ["https://docs.mrx8086.com/*"] },
-                { id: NODERED_CLIENT_ID, name: "Node-RED", redirectUris: ["https://automate.mrx8086.com/*"] }
-            ];
-
-            for (const client of clients) {
-                const clientExists = await checkClientExists(token, client.id);
-                if (!clientExists) {
-                    await createClient(token, client.id, client.name, client.redirectUris);
+        } else {
+            console.log('Realm already exists, skipping base setup');
+        }
+
+        // Clients erstellen
+        const clients = [
+            { id: NEXTCLOUD_CLIENT_ID, name: "nextcloud", redirectUris: ["https://cloud.mrx8086.com/*"] },
+            { id: PAPERLESS_CLIENT_ID, name: "paperless", redirectUris: ["https://docs.mrx8086.com/*"] },
+            { id: NODERED_CLIENT_ID, name: "nodered", redirectUris: ["https://automate.mrx8086.com/*"] }
+        ];
+        for (const client of clients) {
+            await createClient(token, client.id, client.name, client.redirectUris);
+        }
+
+        // Gruppen erstellen
+        await createDefaultGroups(token);
+
+        // Test User erstellen
+        await createInitialUsers(token);
+
+        // Client Mapper überprüfen
+        const clientMappers = await getClientMappers(token, NEXTCLOUD_CLIENT_ID);
+        if (clientMappers) {
+            console.log("Client Mappers:", clientMappers);
+            const groupMapper = clientMappers.find(mapper => mapper.name === 'groups');
+            if (!groupMapper) {
+                console.error("Error: 'groups' mapper not found for Nextcloud client");
+            } else {
+                console.log("'groups' mapper found")
+                if (groupMapper.config && groupMapper.config.fullGroupPath != "true") {
+                    console.warn("Warning: 'Full group path' in mapper 'groups' is not enabled.");
                 } else {
-                    console.log(`Client ${client.id} already exists`);
+                    console.log("'Full group path' in mapper 'groups' is enabled");
                 }
             }
+        } else {
+            console.error("Error getting Client Mappers");
+        }
 
-            // Gruppen erstellen
-            await createDefaultGroups(token);
+        // Konfiguration des Office-Automation Realms mit Admin Token auslesen
+        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');
+            }
         } else {
-            console.log('Realm already exists, skipping base setup');
+            console.error("Error getting Master Realm admin token")
+        }
+
+        // Test Token erstellen
+        const testToken = await createTestToken(token, "testadmin@mrx8086.com");
+
+        if (testToken) {
+            console.log("Test Token generated successfully!");
+            const decodedToken = decodeToken(testToken);
+            if (decodedToken)
+                console.log("Token:", decodedToken);
+        } else {
+            console.error("Error generating Test Token");
         }
 
-        // User erstellen/aktualisieren
-        await createInitialUsers(token);
-        
         console.log('Setup completed successfully');
+
     } catch (error) {
         console.error('Setup failed:', error);
         process.exit(1);