Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(release): backport keycloak fixes but without upgrade keycloak client #2429

Merged
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
7 changes: 7 additions & 0 deletions .changeset/early-trainers-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@janus-idp/backstage-plugin-keycloak-backend": major
AndrienkoAleksandr marked this conversation as resolved.
Show resolved Hide resolved
---

Provide keycloak-backend fixes:
- avoid undefined values for keycloak group members
- retrieve full list group members using pagination
8 changes: 2 additions & 6 deletions plugins/keycloak-backend/__fixtures__/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,5 @@ export const users = [
},
];

export const groupMembers = [
['jamesdoe'],
[],
[],
['jamesdoe', 'joedoe', 'johndoe'],
];
export const groupMembers1 = ['jamesdoe'];
export const groupMembers2 = ['jamesdoe', 'joedoe', 'johndoe'];
33 changes: 28 additions & 5 deletions plugins/keycloak-backend/__fixtures__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getVoidLogger } from '@backstage/backend-common';
import { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';

import { groupMembers, groups, users } from './data';
import { groupMembers1, groupMembers2, groups, users } from './data';

export const BASIC_VALID_CONFIG = {
catalog: {
Expand Down Expand Up @@ -55,10 +55,25 @@ export class KeycloakAdminClientMock {
count: jest.fn().mockResolvedValue(groups.length),
listMembers: jest
.fn()
.mockResolvedValueOnce(groupMembers[0].map(username => ({ username })))
.mockResolvedValueOnce(groupMembers[1].map(username => ({ username })))
.mockResolvedValueOnce(groupMembers[2].map(username => ({ username })))
.mockResolvedValueOnce(groupMembers[3].map(username => ({ username }))),
.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;
Expand All @@ -74,6 +89,14 @@ class FakeAbortSignal implements AbortSignal {
dispatchEvent() {
return true;
}
any(signals: Iterable<AbortSignal>): AbortSignal {
for (const signal of signals) {
if (signal.aborted) {
return signal;
}
}
throw new Error('No abort signal found');
}
}

export class ManualTaskRunner implements TaskRunner {
Expand Down
2 changes: 1 addition & 1 deletion plugins/keycloak-backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@janus-idp/backstage-plugin-keycloak-backend",
"version": "1.13.3",
"version": "1.13.4",
AndrienkoAleksandr marked this conversation as resolved.
Show resolved Hide resolved
"description": "A Backend backend plugin for Keycloak",
"main": "src/index.ts",
"types": "src/index.ts",
Expand Down
23 changes: 16 additions & 7 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 KcAdminClient from '@keycloak/keycloak-admin-client';

import {
Expand All @@ -21,10 +23,12 @@ const config: KeycloakProviderConfig = {
baseUrl: 'http://mock-url',
};

const logger = mockServices.logger.mock();

describe('readKeycloakRealm', () => {
it('should return the correct number of users and groups', async () => {
const client = new KeycloakAdminClientMock() as unknown as KcAdminClient;
const { users, groups } = await readKeycloakRealm(client, config);
const { users, groups } = await readKeycloakRealm(client, config, logger);

expect(users).toHaveLength(3);
expect(groups).toHaveLength(4);
Expand All @@ -41,7 +45,7 @@ describe('readKeycloakRealm', () => {
};

const client = new KeycloakAdminClientMock() as unknown as KcAdminClient;
const { users, groups } = await readKeycloakRealm(client, config, {
const { users, groups } = await readKeycloakRealm(client, config, logger, {
userTransformer,
groupTransformer,
});
Expand Down Expand Up @@ -144,11 +148,15 @@ describe('getEntities', () => {
it('should fetch all users', async () => {
const client = new KeycloakAdminClientMock() as unknown as KcAdminClient;

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 @@ -163,6 +171,7 @@ describe('getEntities', () => {
baseUrl: '',
realm: '',
},
logger,
1,
);

Expand Down
81 changes: 63 additions & 18 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 @@ -120,6 +121,7 @@ export function* traverseGroups(
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 @@ -132,11 +134,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 @@ -146,9 +152,43 @@ export async function getEntities<T extends Users | Groups>(
return entityResults;
}

async function getAllGroupMembers<T extends Groups>(
groups: T,
groupId: string,
config: KeycloakProviderConfig,
options?: { userQuerySize?: number },
): Promise<string[]> {
const querySize = options?.userQuerySize || 100;

let allMembers: string[] = [];
let page = 0;
let totalMembers = 0;

do {
const members = await groups.listMembers({
id: groupId,
max: querySize,
realm: config.realm,
first: page * querySize,
});

if (members.length > 0) {
allMembers = allMembers.concat(members.map(m => m.username!));
totalMembers = members.length; // Get the number of members retrieved
} else {
totalMembers = 0; // No members retrieved
}

page++;
} while (totalMembers > 0);

return allMembers;
}

export const readKeycloakRealm = async (
client: KeycloakAdminClient,
config: KeycloakProviderConfig,
logger: LoggerService,
options?: {
userQuerySize?: number;
groupQuerySize?: number;
Expand All @@ -162,12 +202,14 @@ export const readKeycloakRealm = async (
const kUsers = await getEntities(
client.users,
config,
logger,
options?.userQuerySize,
);

const rawKGroups = await getEntities(
client.groups,
config,
logger,
options?.groupQuerySize,
);
const flatKGroups = rawKGroups.reduce((acc, g) => {
Expand All @@ -176,13 +218,12 @@ export const readKeycloakRealm = async (
}, [] as GroupRepresentationWithParent[]);
const kGroups = await Promise.all(
flatKGroups.map(async g => {
g.members = (
await client.groups.listMembers({
id: g.id!,
max: options?.userQuerySize,
realm: config.realm,
})
).map(m => m.username!);
g.members = await getAllGroupMembers(
client.groups as Groups,
g.id!,
config,
options,
);
return g;
}),
);
Expand Down Expand Up @@ -228,13 +269,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 @@ -217,12 +217,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