janus-idp / backstage-plugins

Plugins for Backstage
https://janus-idp.io
Apache License 2.0
151 stars 149 forks source link

Keycloak backend plugin leads to DOS of Keycloak server #2471

Open JohannesWill opened 2 weeks ago

JohannesWill commented 2 weeks ago

Describe the bug

I have setup the Keycloak backend plugin by following the steps given on below this link - https://janus-idp.io/plugins/keycloak/ .

Syncing the users and Groups will overload our Keycloak instance by doing many parrallel requests

Expected Behavior

What are the steps to reproduce this bug?

  1. Keycloak with many users and groups (in my case ~2500 users and 850 groups)
  2. Configure the Keycloak plugin for new backup configuration (using steps from https://janus-idp.io/plugins/keycloak/)

Versions of software used and environment @janus-idp/backstage-plugin-keycloak-backend": "^2.0.8",

Backstage - 1.30.4 (create-app@0.5.18)

Temporary Workaround: During investigation of that problem I used patch-package to patch @janus-idp/backstage-plugin-keycloak-backend@2.0.8 for the project I'm working on. With that pacht, where I serialized the calls to Keycloak, the problem is not reproduceable anymore.

Here is the diff that solved my problem:

diff --git a/node_modules/@janus-idp/backstage-plugin-keycloak-backend/dist/index.cjs.js b/node_modules/@janus-idp/backstage-plugin-keycloak-backend/dist/index.cjs.js
index a0e323e..180abfc 100644
--- a/node_modules/@janus-idp/backstage-plugin-keycloak-backend/dist/index.cjs.js
+++ b/node_modules/@janus-idp/backstage-plugin-keycloak-backend/dist/index.cjs.js
@@ -153,17 +153,19 @@ async function getEntities(entities, config, logger, entityQuerySize = KEYCLOAK_
   const rawEntityCount = await entities.count({ realm: config.realm });
   const entityCount = typeof rawEntityCount === "number" ? rawEntityCount : rawEntityCount.count;
   const pageCount = Math.ceil(entityCount / entityQuerySize);
-  const entityPromises = Array.from(
-    { length: pageCount },
-    (_, i) => entities.find({
-      realm: config.realm,
-      max: entityQuerySize,
-      first: i * entityQuerySize
-    }).catch(
-      (err) => logger.warn("Failed to retieve Keycloak entities.", err)
-    )
-  );
-  const entityResults = (await Promise.all(entityPromises)).flat();
+  const entityResults = [];
+  for (let i = 0; i < pageCount; i++) {
+    try {
+      const entitiesPage = await entities.find({
+        realm: config.realm,
+        max: entityQuerySize,
+        first: i * entityQuerySize
+      });
+      entityResults.push(...entitiesPage);
+    } catch (err) {
+      logger.warn("Failed to retrieve Keycloak entities.", err);
+    }
+  }
   return entityResults;
 }
 async function getAllGroupMembers(groups, groupId, config, options) {
@@ -254,35 +256,36 @@ const readKeycloakRealm = async (client, config, logger, options) => {
       []
     );
   }
-  const kGroups = await Promise.all(
-    rawKGroups.map(async (g) => {
-      g.members = await getAllGroupMembers(
-        client.groups,
-        g.id,
-        config,
-        options
-      );
-      if (isVersion23orHigher) {
-        if (g.subGroupCount > 0) {
-          g.subGroups = await client.groups.listSubGroups({
-            parentId: g.id,
-            first: 0,
-            max: g.subGroupCount,
-            briefRepresentation: false,
-            realm: config.realm
-          });
-        }
-        if (g.parentId) {
-          const groupParent = await client.groups.findOne({
-            id: g.parentId,
-            realm: config.realm
-          });
-          g.parent = groupParent?.name;
-        }
+  const kGroups = [];
+  for (const g of rawKGroups) {
+    g.members = await getAllGroupMembers(
+      client.groups,
+      g.id,
+      config,
+      options,
+    );
+
+    if (isVersion23orHigher) {
+      if (g.subGroupCount > 0) {
+        g.subGroups = await client.groups.listSubGroups({
+          parentId: g.id,
+          first: 0,
+          max: g.subGroupCount,
+          briefRepresentation: false,
+          realm: config.realm,
+        });
+      }ö
+      if (g.parentId) {
+        const groupParent = await client.groups.findOne({
+          id: g.parentId,
+          realm: config.realm,
+        });
+        g.parent = groupParent?.name;
       }
-      return g;
-    })
-  );
+    }
+
+    kGroups.push(g);
+  }
   const parsedGroups = await kGroups.reduce(
     async (promise, g) => {
       const partial = await promise;

This issue body was partially generated by patch-package.

JohannesWill commented 5 days ago

I have prepared a fix for the issue. Should I fix it in this repo or https://github.com/backstage/community-plugins/tree/main/workspaces/keycloak @04kash @AndrienkoAleksandr

04kash commented 4 days ago

Hi @JohannesWill, this plugin has been migrated to https://github.com/backstage/community-plugins/tree/main/workspaces/keycloak and won't be maintained in this repository, so feel free to open a PR with your fix there!

04kash commented 4 days ago

Additionally, could you raise this bug in the https://github.com/backstage/community-plugins repo and close this one?