setup_realm.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. import dotenv from 'dotenv';
  2. import axios from 'axios';
  3. // Load environment variables
  4. dotenv.config();
  5. console.log('Environment variables loaded.');
  6. // Configuration constants
  7. const KEYCLOAK_URL = process.env.KEYCLOAK_URL || 'https://auth.mrx8086.com';
  8. const ADMIN_USERNAME = process.env.KEYCLOAK_ADMIN_USER;
  9. const ADMIN_PASSWORD = process.env.KEYCLOAK_ADMIN_PASSWORD;
  10. const REALM_NAME = 'office-automation';
  11. console.log('Configuration constants set:', { KEYCLOAK_URL, ADMIN_USERNAME, REALM_NAME });
  12. // Client ID for Paperless
  13. const PAPERLESS_CLIENT_ID = process.env.PAPERLESS_CLIENT_ID || 'paperless';
  14. // Helper function for API error handling
  15. const handleAxiosError = (error, operation, config, response) => {
  16. console.error(`Error during ${operation}:`);
  17. if (config) {
  18. console.error('Request:', {
  19. method: config.method,
  20. url: config.url,
  21. headers: config.headers,
  22. data: config.data,
  23. });
  24. }
  25. if (error.response) {
  26. console.error('Response:', {
  27. status: error.response.status,
  28. data: error.response.data
  29. });
  30. } else {
  31. console.error('Error Message:', error.message);
  32. }
  33. throw error;
  34. };
  35. // Get Admin Token
  36. async function getAdminToken() {
  37. console.log('Getting admin token...');
  38. try {
  39. const response = await axios.post(
  40. `${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
  41. new URLSearchParams({
  42. 'client_id': 'admin-cli',
  43. 'username': ADMIN_USERNAME,
  44. 'password': ADMIN_PASSWORD,
  45. 'grant_type': 'password'
  46. }),
  47. {
  48. headers: {
  49. 'Content-Type': 'application/x-www-form-urlencoded'
  50. }
  51. }
  52. );
  53. console.log('Admin token received.');
  54. return response.data.access_token;
  55. } catch (error) {
  56. handleAxiosError(error, 'getting admin token');
  57. }
  58. }
  59. // Function to get client information by clientId
  60. async function getClientByClientId(token, clientId) {
  61. console.log(`Getting client information for ${clientId}...`);
  62. try {
  63. const response = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`, {
  64. headers: { 'Authorization': `Bearer ${token}` },
  65. params: { clientId }
  66. });
  67. if (response.data.length === 0) {
  68. console.log(`Client ${clientId} not found`);
  69. return null;
  70. }
  71. console.log(`Client ${clientId} found.`);
  72. return response.data[0];
  73. } catch (error) {
  74. handleAxiosError(error, `getting client ${clientId}`);
  75. return null;
  76. }
  77. }
  78. // Check if client exists
  79. const checkClientExists = async (token, clientId) => !!await getClientByClientId(token, clientId);
  80. // Get client mappers by client ID
  81. async function getClientMappers(token, clientId) {
  82. console.log(`Getting client mappers for ${clientId}...`);
  83. const client = await getClientByClientId(token, clientId);
  84. if (!client) {
  85. console.log(`Client ${clientId} not found, no mappers to get.`);
  86. return [];
  87. }
  88. try {
  89. const response = await axios.get(
  90. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
  91. { headers: { 'Authorization': `Bearer ${token}` } }
  92. );
  93. console.log(`Client mappers for ${clientId} retrieved.`);
  94. return response.data;
  95. } catch (error) {
  96. handleAxiosError(error, `getting client mappers for ${clientId}`, error.config, error.response);
  97. return [];
  98. }
  99. }
  100. // Get client scopes for a client
  101. async function getClientScopes(token, clientId) {
  102. console.log(`Getting client scopes for ${clientId}...`);
  103. const client = await getClientByClientId(token, clientId);
  104. if (!client) {
  105. console.log(`Client ${clientId} not found, no client scopes to get.`);
  106. return [];
  107. }
  108. try {
  109. const response = await axios.get(
  110. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/client-scopes`,
  111. { headers: { 'Authorization': `Bearer ${token}` } }
  112. );
  113. console.log(`Client scopes for ${clientId} retrieved.`);
  114. return response.data;
  115. } catch (error) {
  116. handleAxiosError(error, `getting client scopes for ${clientId}`, error.config, error.response);
  117. return [];
  118. }
  119. }
  120. // Get a specific client scope by name
  121. async function getClientScope(token, scopeName) {
  122. console.log(`Getting client scope "${scopeName}"...`);
  123. try {
  124. const response = await axios.get(
  125. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
  126. { headers: { 'Authorization': `Bearer ${token}` } }
  127. );
  128. const foundScope = response.data.find(scope => scope.name === scopeName);
  129. if (!foundScope) {
  130. console.log(`Client Scope "${scopeName}" not found`);
  131. return null;
  132. }
  133. console.log(`Client scope "${scopeName}" found:`, foundScope);
  134. return foundScope;
  135. } catch (error) {
  136. handleAxiosError(error, `getting client scope ${scopeName}`, error.config, error.response);
  137. return null;
  138. }
  139. }
  140. // Add a default client scope to a client
  141. async function addDefaultClientScope(token, clientId, scopeName) {
  142. console.log(`Adding client scope "${scopeName}" as default for client "${clientId}"...`);
  143. const client = await getClientByClientId(token, clientId);
  144. const scope = await getClientScope(token, scopeName);
  145. if (!client || !scope) {
  146. console.log(`Client "${clientId}" or scope "${scopeName}" not found, cannot add as default scope.`);
  147. return;
  148. }
  149. try {
  150. await axios.put(
  151. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/default-client-scopes/${scope.id}`,
  152. null,
  153. {
  154. headers: {
  155. 'Authorization': `Bearer ${token}`,
  156. 'Content-Type': 'application/json'
  157. }
  158. }
  159. );
  160. console.log(`Client scope "${scopeName}" added as default scope for client "${clientId}"`);
  161. } catch (error) {
  162. handleAxiosError(error, `adding client scope "${scopeName}" as default for client "${clientId}"`);
  163. }
  164. }
  165. // Create client and manage mappers
  166. async function createClient(token, clientId, clientName, redirectUris) {
  167. console.log(`Creating client "${clientId}"...`);
  168. let client = await getClientByClientId(token, clientId);
  169. if (!client) {
  170. const clientConfig = {
  171. clientId: clientId,
  172. name: clientName,
  173. enabled: true,
  174. protocol: "openid-connect",
  175. publicClient: false,
  176. authorizationServicesEnabled: false,
  177. serviceAccountsEnabled: false,
  178. standardFlowEnabled: true,
  179. implicitFlowEnabled: false,
  180. directAccessGrantsEnabled: true,
  181. redirectUris: redirectUris,
  182. webOrigins: ["+"],
  183. defaultClientScopes: ["roles"],
  184. optionalClientScopes: ["address", "phone", "offline_access", "microprofile-jwt"],
  185. rootUrl: process.env.PAPERLESS_URL,
  186. baseUrl: process.env.PAPERLESS_URL,
  187. adminUrl: process.env.PAPERLESS_URL,
  188. };
  189. try {
  190. const response = await axios.post(
  191. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients`,
  192. clientConfig,
  193. {
  194. headers: {
  195. 'Authorization': `Bearer ${token}`,
  196. 'Content-Type': 'application/json'
  197. }
  198. }
  199. );
  200. console.log(`Client "${clientId}" created successfully`);
  201. client = response.data;
  202. } catch (error) {
  203. handleAxiosError(error, `creating client: ${clientId}`);
  204. return;
  205. }
  206. } else {
  207. console.log(`Client "${clientId}" already exists, checking mappers and client secret`);
  208. }
  209. if (client) {
  210. try {
  211. await axios.put(
  212. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}`,
  213. { ...client, secret: process.env[`KEYCLOAK_${clientId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()}_CLIENT_SECRET`] },
  214. {
  215. headers: {
  216. 'Authorization': `Bearer ${token}`,
  217. 'Content-Type': 'application/json'
  218. }
  219. }
  220. );
  221. console.log(`Set client secret for client: ${clientId}`);
  222. } catch (error) {
  223. handleAxiosError(error, `setting client secret for client: ${clientId}`, error.config, error.response);
  224. }
  225. const existingMappers = await getClientMappers(token, clientId);
  226. const requiredMappers = [
  227. {
  228. name: "groups",
  229. protocol: "openid-connect",
  230. protocolMapper: "oidc-group-membership-mapper",
  231. config: {
  232. "full.path": "true",
  233. "id.token.claim": "true",
  234. "access.token.claim": "true",
  235. "userinfo.token.claim": "true",
  236. "claim.name": "groups"
  237. }
  238. },
  239. {
  240. name: "realm roles",
  241. protocol: "openid-connect",
  242. protocolMapper: "oidc-usermodel-realm-role-mapper",
  243. config: {
  244. "id.token.claim": "true",
  245. "access.token.claim": "true",
  246. "userinfo.token.claim": "true",
  247. "claim.name": "roles",
  248. "jsonType.label": "String",
  249. "multivalued": "true"
  250. }
  251. }
  252. ];
  253. for (const mapper of requiredMappers) {
  254. const existingMapper = existingMappers.find(m => m.name === mapper.name);
  255. try {
  256. if (existingMapper) {
  257. await axios.put(
  258. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models/${existingMapper.id}`,
  259. { ...existingMapper, ...mapper },
  260. {
  261. headers: {
  262. 'Authorization': `Bearer ${token}`,
  263. 'Content-Type': 'application/json'
  264. }
  265. }
  266. );
  267. console.log(`Mapper "${mapper.name}" updated for client "${clientId}"`);
  268. } else {
  269. await axios.post(
  270. `${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/clients/${client.id}/protocol-mappers/models`,
  271. mapper,
  272. {
  273. headers: {
  274. 'Authorization': `Bearer ${token}`,
  275. 'Content-Type': 'application/json'
  276. }
  277. }
  278. );
  279. console.log(`Mapper "${mapper.name}" created for client "${clientId}"`);
  280. }
  281. } catch (error) {
  282. handleAxiosError(error, `managing mapper "${mapper.name}" for client "${clientId}"`, error.config, error.response);
  283. }
  284. }
  285. await addDefaultClientScope(token, clientId, "openid");
  286. await addDefaultClientScope(token, clientId, "profile");
  287. await addDefaultClientScope(token, clientId, "groups-paperless");
  288. }
  289. }
  290. // Create default groups
  291. async function createDefaultGroups(token) {
  292. console.log('Creating default groups...');
  293. const groups = [
  294. { name: "paperless-admins", path: "/paperless-admins", attributes: { "description": ["Paperless administrators"] } },
  295. { name: "paperless-users", path: "/paperless-users", attributes: { "description": ["Paperless regular users"] } },
  296. { name: "paperless-service", path: "/paperless-service", attributes: { "description": ["Paperless service user"] } }
  297. ];
  298. for (const group of groups) {
  299. try {
  300. const existingGroups = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, {
  301. headers: { 'Authorization': `Bearer ${token}` },
  302. params: { search: group.name, exact: true } // Added exact: true for precise matching
  303. });
  304. if (existingGroups.data.length > 0) {
  305. console.log(`Group "${group.name}" already exists`);
  306. continue;
  307. }
  308. await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, group, {
  309. headers: {
  310. 'Authorization': `Bearer ${token}`,
  311. 'Content-Type': 'application/json'
  312. }
  313. });
  314. console.log(`Group "${group.name}" created successfully`);
  315. } catch (error) {
  316. if (error.response?.status === 409) {
  317. console.log(`Group "${group.name}" already exists`);
  318. } else {
  319. handleAxiosError(error, `creating group: ${group.name}`);
  320. }
  321. }
  322. }
  323. }
  324. // Function to add users to groups
  325. async function addUsersToGroups(token) {
  326. console.log('Adding users to groups...');
  327. const users = [
  328. { username: "testadmin", groups: ["paperless-admins", "paperless-users"] },
  329. { username: "testuser", groups: ["paperless-users"] },
  330. { username: "testserviceuser", groups: ["paperless-service"] }
  331. ];
  332. for (const user of users) {
  333. try {
  334. const existingUsers = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users`, {
  335. headers: { 'Authorization': `Bearer ${token}` },
  336. params: { username: user.username, exact: true } // Added exact: true for precise matching
  337. });
  338. if (existingUsers.data.length === 0) {
  339. console.log(`User "${user.username}" not found`);
  340. continue;
  341. }
  342. const userId = existingUsers.data[0].id;
  343. for (const groupName of user.groups) {
  344. const groupsResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/groups`, {
  345. headers: { 'Authorization': `Bearer ${token}` },
  346. params: { search: groupName, exact: true }
  347. });
  348. if (groupsResponse.data.length > 0) {
  349. const groupId = groupsResponse.data[0].id;
  350. try {
  351. await axios.put(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/users/${userId}/groups/${groupId}`, {}, {
  352. headers: {
  353. 'Authorization': `Bearer ${token}`,
  354. 'Content-Type': 'application/json'
  355. }
  356. });
  357. console.log(`User "${user.username}" added to group "${groupName}"`);
  358. } catch (error) {
  359. handleAxiosError(error, `adding user "${user.username}" to group "${groupName}"`, error.config, error.response);
  360. }
  361. } else {
  362. console.log(`Group "${groupName}" not found`);
  363. }
  364. }
  365. } catch (error) {
  366. handleAxiosError(error, `adding user "${user.username}" to groups`, error.config, error.response);
  367. }
  368. }
  369. }
  370. async function createGroupsPaperlessScope(token) {
  371. const scopeName = "groups-paperless";
  372. const mapperName = "groups-mapper";
  373. console.log(`Starting createGroupsPaperlessScope`);
  374. let clientScope = await getClientScope(token, scopeName);
  375. if (!clientScope) {
  376. try {
  377. console.log(`Creating client scope "${scopeName}"`);
  378. clientScope = await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes`,
  379. {
  380. "name": scopeName,
  381. "protocol": "openid-connect",
  382. "description": "Provides access to user group information for Paperless.", // Hinzugefügt: Beschreibung
  383. "attributes": {},
  384. "consentScreenText": "Grant access to user groups in Paperless",
  385. "includeInTokenScope": true
  386. },
  387. {
  388. headers: {
  389. 'Authorization': `Bearer ${token}`,
  390. 'Content-Type': 'application/json'
  391. }
  392. });
  393. console.log(`Client scope "${scopeName}" created successfully`);
  394. clientScope = response.data;
  395. } catch (error) {
  396. console.error(`Error creating client scope "${scopeName}":`, error);
  397. handleAxiosError(error, `creating ${scopeName} client scope`, error.config, error.response);
  398. return;
  399. }
  400. } else {
  401. console.log(`Client scope "${scopeName}" exists, skipping creation`);
  402. }
  403. console.log("Client scope creation step finished");
  404. if (clientScope) {
  405. console.log(`Check for mapper "${mapperName}" in scope "${scopeName}"`);
  406. try {
  407. const mappersResponse = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
  408. { headers: { 'Authorization': `Bearer ${token}` } }
  409. );
  410. if (!mappersResponse.data.find(m => m.name === mapperName)) {
  411. try {
  412. await axios.post(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}/client-scopes/${clientScope.id}/protocol-mappers/models`,
  413. {
  414. "name": mapperName,
  415. "protocol": "openid-connect",
  416. "protocolMapper": "oidc-group-membership-mapper",
  417. "config": {
  418. "full.path": "false",
  419. "id.token.claim": "false",
  420. "access.token.claim": "true",
  421. "userinfo.token.claim": "true",
  422. "claim.name": "groups",
  423. "add.to.introspection": "false"
  424. }
  425. },
  426. {
  427. headers: {
  428. 'Authorization': `Bearer ${token}`,
  429. 'Content-Type': 'application/json'
  430. }
  431. });
  432. console.log(`Mapper "${mapperName}" created for client scope "${scopeName}"`);
  433. } catch (error) {
  434. console.error(`Error creating mapper "${mapperName}" for scope "${scopeName}":`, error);
  435. handleAxiosError(error, `creating mapper for ${scopeName}`, error.config, error.response);
  436. }
  437. } else {
  438. console.log(`Mapper "${mapperName}" exists in client scope "${scopeName}", skipping creation`);
  439. }
  440. } catch (error) {
  441. console.error("Error checking for mappers:", error);
  442. handleAxiosError(error, `checking mappers for ${scopeName}`, error.config, error.response);
  443. }
  444. }
  445. console.log("Finished createGroupsPaperlessScope");
  446. }
  447. // Main function
  448. async function setupPaperlessRealm() {
  449. try {
  450. console.log('Starting Paperless Keycloak setup...');
  451. const token = await getAdminToken();
  452. // Create Paperless client
  453. await createClient(token, PAPERLESS_CLIENT_ID, PAPERLESS_CLIENT_ID, [`https://docs.mrx8086.com/*`]);
  454. // Create groups
  455. await createDefaultGroups(token);
  456. // Create client scope groups-paperless
  457. await createGroupsPaperlessScope(token);
  458. // Add users to groups
  459. await addUsersToGroups(token);
  460. // Read the configuration of the Office-Automation realm with Admin Token
  461. if (token) {
  462. console.log("Master Realm Admin Token:", token);
  463. try {
  464. const realmConfig = await axios.get(`${KEYCLOAK_URL}/admin/realms/${REALM_NAME}`, {
  465. headers: { 'Authorization': `Bearer ${token}` }
  466. });
  467. console.log("Office Automation Realm Configuration:", realmConfig.data);
  468. } catch (error) {
  469. handleAxiosError(error, 'getting office realm configuration', error.config, error.response);
  470. }
  471. } else {
  472. console.error("Error getting Master Realm admin token");
  473. }
  474. console.log('Paperless Keycloak setup completed successfully');
  475. } catch (error) {
  476. console.error('Paperless Keycloak setup failed:', error);
  477. process.exit(1);
  478. }
  479. }
  480. // Execute the script
  481. setupPaperlessRealm();