Skip to content

Commit

Permalink
fix(keycloak): avoid undefined values for keycloak group members (#2319)
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
  • Loading branch information
AndrienkoAleksandr authored Oct 9, 2024
1 parent 46175f2 commit 3447ba8
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 31 deletions.
59 changes: 45 additions & 14 deletions plugins/keycloak-backend/src/lib/read.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { mockServices } from '@backstage/backend-test-utils';

import KeycloakAdminClient from '@keycloak/keycloak-admin-client';

import {
Expand Down Expand Up @@ -28,23 +30,41 @@ const config: KeycloakProviderConfig = {
baseUrl: 'http://mock-url',
};

const logger = mockServices.logger.mock();

describe('readKeycloakRealm', () => {
it('should return the correct number of users and groups (Version 23 or Higher)', async () => {
const client =
new KeycloakAdminClientMockServerv24() as unknown as KeycloakAdminClient;
const { users, groups } = await readKeycloakRealm(client, config);
const { users, groups } = await readKeycloakRealm(client, config, logger);
expect(users).toHaveLength(3);
expect(groups).toHaveLength(3);
});

it('should return the correct number of users and groups (Version Less than 23)', async () => {
const client =
new KeycloakAdminClientMockServerv18() as unknown as KeycloakAdminClient;
const { users, groups } = await readKeycloakRealm(client, config);
const { users, groups } = await readKeycloakRealm(client, config, logger);
expect(users).toHaveLength(3);
expect(groups).toHaveLength(3);
});

it(`should not contain undefined members when a group member is not found in the fetched user list`, async () => {
const client =
new KeycloakAdminClientMockServerv24() as unknown as KeycloakAdminClient;
client.users.find = jest
.fn()
.mockResolvedValue([usersFixture[1], usersFixture[2]]);
client.users.count = jest.fn().mockResolvedValue(2);

const { groups } = await readKeycloakRealm(client, config, logger);

for (const group of groups) {
console.log(group.spec.members);
expect(group.spec.members).not.toContain(undefined);
}
});

it('should propagate transformer changes to entities (version 23 or Higher)', async () => {
const groupTransformer: GroupTransformer = async (entity, _g, _r) => {
entity.metadata.name = `${entity.metadata.name}_foo`;
Expand All @@ -57,7 +77,7 @@ describe('readKeycloakRealm', () => {

const client =
new KeycloakAdminClientMockServerv24() as unknown as KeycloakAdminClient;
const { users, groups } = await readKeycloakRealm(client, config, {
const { users, groups } = await readKeycloakRealm(client, config, logger, {
userTransformer,
groupTransformer,
});
Expand All @@ -81,7 +101,7 @@ describe('readKeycloakRealm', () => {

const client =
new KeycloakAdminClientMockServerv18() as unknown as KeycloakAdminClient;
const { users, groups } = await readKeycloakRealm(client, config, {
const { users, groups } = await readKeycloakRealm(client, config, logger, {
userTransformer,
groupTransformer,
});
Expand Down Expand Up @@ -218,11 +238,15 @@ describe('getEntitiesUser', () => {
const client =
new KeycloakAdminClientMockServerv24() as unknown as KeycloakAdminClient;

const users = await getEntities(client.users, {
id: '',
baseUrl: '',
realm: '',
});
const users = await getEntities(
client.users,
{
id: '',
baseUrl: '',
realm: '',
},
logger,
);

expect(users).toHaveLength(3);
});
Expand All @@ -231,11 +255,15 @@ describe('getEntitiesUser', () => {
const client =
new KeycloakAdminClientMockServerv18() as unknown as KeycloakAdminClient;

const users = await getEntities(client.users, {
id: '',
baseUrl: '',
realm: '',
});
const users = await getEntities(
client.users,
{
id: '',
baseUrl: '',
realm: '',
},
logger,
);

expect(users).toHaveLength(3);
});
Expand All @@ -251,6 +279,7 @@ describe('getEntitiesUser', () => {
baseUrl: '',
realm: '',
},
logger,
1,
);

Expand All @@ -268,6 +297,7 @@ describe('getEntitiesUser', () => {
baseUrl: '',
realm: '',
},
logger,
1,
);

Expand All @@ -282,6 +312,7 @@ describe('fetch subgroups', () => {
const groups = await processGroupsRecursively(
topLevelGroups23orHigher,
client.groups,
config.realm,
);

expect(groups).toHaveLength(3);
Expand Down
40 changes: 29 additions & 11 deletions plugins/keycloak-backend/src/lib/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { LoggerService } from '@backstage/backend-plugin-api';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';

import KeycloakAdminClient from '@keycloak/keycloak-admin-client';
Expand Down Expand Up @@ -109,6 +110,7 @@ export const parseUser = async (
export async function getEntities<T extends Users | Groups>(
entities: T,
config: KeycloakProviderConfig,
logger: LoggerService,
entityQuerySize: number = KEYCLOAK_ENTITY_QUERY_SIZE,
): Promise<Awaited<ReturnType<T['find']>>> {
const rawEntityCount = await entities.count({ realm: config.realm });
Expand All @@ -121,11 +123,15 @@ export async function getEntities<T extends Users | Groups>(
const entityPromises = Array.from(
{ length: pageCount },
(_, i) =>
entities.find({
realm: config.realm,
max: entityQuerySize,
first: i * entityQuerySize,
}) as ReturnType<T['find']>,
entities
.find({
realm: config.realm,
max: entityQuerySize,
first: i * entityQuerySize,
})
.catch(err =>
logger.warn('Failed to retieve Keycloak entities.', err),
) as ReturnType<T['find']>,
);

const entityResults = (await Promise.all(entityPromises)).flat() as Awaited<
Expand All @@ -138,6 +144,7 @@ export async function getEntities<T extends Users | Groups>(
export async function processGroupsRecursively(
topLevelGroups: GroupRepresentationWithParent[],
entities: Groups,
realm: string,
) {
const allGroups: GroupRepresentationWithParent[] = [];
for (const group of topLevelGroups) {
Expand All @@ -149,10 +156,12 @@ export async function processGroupsRecursively(
first: 0,
max: group.subGroupCount,
briefRepresentation: true,
realm,
});
const subGroupResults = await processGroupsRecursively(
subgroups,
entities,
realm,
);
allGroups.push(...subGroupResults);
}
Expand All @@ -174,6 +183,7 @@ export function* traverseGroups(
export const readKeycloakRealm = async (
client: KeycloakAdminClient,
config: KeycloakProviderConfig,
logger: LoggerService,
options?: {
userQuerySize?: number;
groupQuerySize?: number;
Expand All @@ -187,12 +197,14 @@ export const readKeycloakRealm = async (
const kUsers = await getEntities(
client.users,
config,
logger,
options?.userQuerySize,
);

const topLevelKGroups = (await getEntities(
client.groups,
config,
logger,
options?.groupQuerySize,
)) as GroupRepresentationWithParent[];

Expand All @@ -216,6 +228,7 @@ export const readKeycloakRealm = async (
rawKGroups = await processGroupsRecursively(
topLevelKGroups,
client.groups as Groups,
config.realm,
);
} else {
rawKGroups = topLevelKGroups.reduce(
Expand All @@ -240,6 +253,7 @@ export const readKeycloakRealm = async (
first: 0,
max: g.subGroupCount,
briefRepresentation: false,
realm: config.realm,
});
}
if (g.parentId) {
Expand Down Expand Up @@ -296,13 +310,17 @@ export const readKeycloakRealm = async (
const groups = parsedGroups.map(g => {
const entity = g.entity;
entity.spec.members =
g.entity.spec.members?.map(
m => parsedUsers.find(p => p.username === m)?.entity.metadata.name!,
) ?? [];
g.entity.spec.members?.flatMap(m => {
const name = parsedUsers.find(p => p.username === m)?.entity.metadata
.name;
return name ? [name] : [];
}) ?? [];
entity.spec.children =
g.entity.spec.children?.map(
c => parsedGroups.find(p => p.name === c)?.entity.metadata.name!,
) ?? [];
g.entity.spec.children?.flatMap(c => {
const child = parsedGroups.find(p => p.name === c)?.entity.metadata
.name;
return child ? [child] : [];
}) ?? [];
entity.spec.parent = parsedGroups.find(
p => p.name === entity.spec.parent,
)?.entity.metadata.name;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,17 @@ export class KeycloakOrgEntityProvider implements EntityProvider {

await kcAdminClient.auth(credentials);

const { users, groups } = await readKeycloakRealm(kcAdminClient, provider, {
userQuerySize: provider.userQuerySize,
groupQuerySize: provider.groupQuerySize,
userTransformer: this.options.userTransformer,
groupTransformer: this.options.groupTransformer,
});
const { users, groups } = await readKeycloakRealm(
kcAdminClient,
provider,
logger,
{
userQuerySize: provider.userQuerySize,
groupQuerySize: provider.groupQuerySize,
userTransformer: this.options.userTransformer,
groupTransformer: this.options.groupTransformer,
},
);

const { markCommitComplete } = markReadComplete({ users, groups });

Expand Down

0 comments on commit 3447ba8

Please sign in to comment.