Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/keycloak/.changeset/tidy-months-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage-community/plugin-catalog-backend-module-keycloak': patch
---

Fixes group fetching to default to the recursive subgroup strategy when the Keycloak server version cannot be determined from `serverInfo`. Previously, the version check could silently produce an unexpected result if `systemInfo.version` was absent; now the plugin defaults to `processGroupsRecursively` for all versions except those explicitly detected as 22 or lower, which continue to use the flat `traverseGroups` approach.
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,79 @@ export class KeycloakAdminClientMockServerv24 {

auth = authMock;
}

export class KeycloakAdminClientMockServerv26 {
public constructor() {
return;
}

serverInfo = {
find: jest.fn().mockResolvedValue({
systemInfo: {},
}),
};

users = {
find: jest.fn().mockResolvedValue(users),
count: jest.fn().mockResolvedValue(users.length),
};

groups = {
find: jest.fn().mockResolvedValue(topLevelGroups23orHigher),
findOne: jest.fn().mockResolvedValue({
id: '9cf51b5d-e066-4ed8-940c-dc6da77f81a5',
name: 'biggroup',
path: '/biggroup',
subGroupCount: 1,
subGroups: [],
access: {
view: true,
viewMembers: true,
manageMembers: false,
manage: false,
manageMembership: false,
},
}),
count: jest.fn().mockResolvedValue(3),
listSubGroups: jest.fn().mockResolvedValue([
{
id: 'eefa5b46-0509-41d8-b8b3-7ddae9c83632',
name: 'subgroup',
path: '/biggroup/subgroup',
parentId: '9cf51b5d-e066-4ed8-940c-dc6da77f81a5',
subGroupCount: 0,
subGroups: [],
access: {
view: true,
viewMembers: true,
manageMembers: false,
manage: false,
manageMembership: false,
},
},
]),
listMembers: jest
.fn()
.mockImplementation(
async (payload?: {
id: string;
_max?: number;
_realm?: string;
first?: number;
}) => {
const { id, first } = payload || {};
if (id === '9cf51b5d-e066-4ed8-940c-dc6da77f81a5' && first === 0) {
// biggroup - first members page
return groupMembers1.map(username => ({ username }));
}
if (id === 'bb10231b-2939-4b1a-b8bb-9249ed7b76f7' && first === 0) {
// testgroup - first members page
return groupMembers2.map(username => ({ username }));
}
return [];
},
),
};

auth = authMock;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import {
KeycloakAdminClientMockServerv18,
KeycloakAdminClientMockServerv24,
KeycloakAdminClientMockServerv26,
} from '../../__fixtures__/helpers';
import { KeycloakProviderConfig } from './config';
import {
Expand Down Expand Up @@ -121,6 +122,21 @@ describe('readKeycloakRealm', () => {
expect(groups).toHaveLength(3);
});

it('should return the correct number of users and groups (Version 26 or Higher, no systemInfo version)', async () => {
const client =
new KeycloakAdminClientMockServerv26() as unknown as KeycloakAdminClient;
const { users, groups } = await readKeycloakRealm(
client,
config,
logger,
mockPLimit as unknown as LimitFunction,
taskInstanceId,
mockCounter,
);
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;
Expand Down Expand Up @@ -207,6 +223,38 @@ describe('readKeycloakRealm', () => {
expect(users[0].metadata.name).toBe('jamesdoe_bar');
expect(users[0].spec.memberOf).toEqual(['biggroup_foo']);
});

it('should propagate transformer changes to entities (version 26 or higher, no systemInfo version)', async () => {
const groupTransformer: GroupTransformer = async (entity, _g, _r) => {
entity.metadata.name = `${entity.metadata.name}_foo`;
return entity;
};
const userTransformer: UserTransformer = async (e, _u, _r, _g) => {
e.metadata.name = `${e.metadata.name}_bar`;
return e;
};

const client =
new KeycloakAdminClientMockServerv26() as unknown as KeycloakAdminClient;
const { users, groups } = await readKeycloakRealm(
client,
config,
logger,
mockPLimit as unknown as LimitFunction,
taskInstanceId,
mockCounter,
{
userTransformer,
groupTransformer,
},
);
expect(groups[0].metadata.name).toBe('biggroup_foo');
expect(groups[0].spec.children).toEqual(['subgroup_foo']);
expect(groups[0].spec.members).toEqual(['jamesdoe_bar']);
expect(groups[1].spec.parent).toBe('biggroup_foo');
expect(users[0].metadata.name).toBe('jamesdoe_bar');
expect(users[0].spec.memberOf).toEqual(['biggroup_foo']);
});
});

describe('parseGroup', () => {
Expand Down Expand Up @@ -450,4 +498,17 @@ describe('fetch subgroups', () => {

expect(groups).toHaveLength(2);
});

it('processGroupsRecursively (Version 26 or Higher, no systemInfo version)', async () => {
const client =
new KeycloakAdminClientMockServerv26() as unknown as KeycloakAdminClient;
const groups = await processGroupsRecursively(
client,
config,
logger,
topLevelGroups23orHigher,
);

expect(groups).toHaveLength(3);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export const readKeycloakRealm = async (
try {
await ensureTokenValid(client, config, logger);
const serverInfo = await client.serverInfo.find();

serverVersion = parseInt(
serverInfo.systemInfo?.version?.slice(0, 2) || '',
10,
Expand All @@ -302,23 +303,23 @@ export const readKeycloakRealm = async (
throw new Error(`Failed to retrieve Keycloak server information: ${error}`);
}

const isVersion23orHigher = serverVersion >= 23;
const isVersion22orLower = serverVersion <= 22;

let rawKGroups: GroupRepresentationWithParent[] = [];

logger.debug(`Processing groups recursively`);
if (isVersion23orHigher) {
if (isVersion22orLower) {
rawKGroups = topLevelKGroups.reduce(
(acc, g) => acc.concat(...traverseGroups(g)),
[] as GroupRepresentationWithParent[],
);
} else {
rawKGroups = await processGroupsRecursively(
client,
config,
logger,
topLevelKGroups,
);
} else {
rawKGroups = topLevelKGroups.reduce(
(acc, g) => acc.concat(...traverseGroups(g)),
[] as GroupRepresentationWithParent[],
);
}

logger.debug(`Fetching group members for keycloak groups and list subgroups`);
Expand All @@ -338,7 +339,7 @@ export const readKeycloakRealm = async (
options,
);

if (isVersion23orHigher) {
if (!isVersion22orLower) {
if (g.subGroupCount! > 0) {
await ensureTokenValid(client, config, logger);
g.subGroups = await client.groups.listSubGroups({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
CONFIG,
KeycloakAdminClientMockServerv18,
KeycloakAdminClientMockServerv24,
KeycloakAdminClientMockServerv26,
PASSWORD_CONFIG,
} from '../../__fixtures__/helpers';
import { KeycloakOrgEntityProvider } from './KeycloakOrgEntityProvider';
Expand Down Expand Up @@ -66,6 +67,7 @@ const scheduler = mockServices.scheduler.mock({

describe.each([
['v24', KeycloakAdminClientMockServerv24],
['v26', KeycloakAdminClientMockServerv26],
['v18', KeycloakAdminClientMockServerv18],
])('KeycloakOrgEntityProvider with %s', (_version, MockImplementation) => {
let logger: ServiceMock<LoggerService>;
Expand Down
Loading
Loading