Skip to content

Commit 728273d

Browse files
committed
feat: added script to run keyclock configuration on init
Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
1 parent a6a83bf commit 728273d

8 files changed

Lines changed: 220 additions & 8 deletions

File tree

apps/api-gateway/src/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { EcosystemSwaggerFilter } from './authz/guards/ecosystem-swagger.filter'
1919
import { FidoModule } from './fido/fido.module';
2020
import { GeoLocationModule } from './geo-location/geo-location.module';
2121
import { GlobalConfigModule } from '@credebl/config/global-config.module';
22+
import { KeycloakConfigModule } from '@credebl/keycloak-config';
2223
import { IssuanceModule } from './issuance/issuance.module';
2324
import { LoggerModule } from '@credebl/logger/logger.module';
2425
import { NotificationModule } from './notification/notification.module';
@@ -73,7 +74,8 @@ import { shouldLoadOidcModules } from '@credebl/common/common.utils';
7374
CloudWalletModule,
7475
ConditionalModule.registerWhen(Oid4vcIssuanceModule, shouldLoadOidcModules),
7576
ConditionalModule.registerWhen(Oid4vpModule, shouldLoadOidcModules),
76-
ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules)
77+
ConditionalModule.registerWhen(X509Module, shouldLoadOidcModules),
78+
KeycloakConfigModule
7779
],
7880
controllers: [AppController],
7981
providers: [

apps/ecosystem/src/ecosystem.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ export class EcosystemService {
197197
throw new Error('Error fetching user');
198198
}
199199

200-
if (!invitation) {
201-
throw new ForbiddenException(ResponseMessages.ecosystem.error.invitationRequired);
202-
}
200+
// if (!invitation) {
201+
// throw new ForbiddenException(ResponseMessages.ecosystem.error.invitationRequired);
202+
// }
203203

204204
const ecosystem = await this.prisma.$transaction(async (tx) => {
205205
const newEcosystem = await this.ecosystemRepository.createNewEcosystem(createEcosystemDto, tx);
@@ -469,8 +469,8 @@ export class EcosystemService {
469469
throw new BadRequestException(ResponseMessages.ecosystem.error.alreadyAccepted);
470470
}
471471
const result = await this.ecosystemRepository.updateEcosystemInvitationStatusByEmail(
472-
orgId,
473472
userEmail,
473+
orgId,
474474
ecosystemId,
475475
status
476476
);

libs/common/src/nats.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ export const getNatsOptions = (
99
authenticator?: Authenticator;
1010
maxReconnectAttempts: NATSReconnects;
1111
reconnectTimeWait: NATSReconnects;
12-
// queue?: string;
12+
queue?: string;
1313
} => {
1414
const baseOptions = {
1515
servers: `${process.env.NATS_URL}`.split(','),
1616
maxReconnectAttempts: NATSReconnects.maxReconnectAttempts,
17-
reconnectTimeWait: NATSReconnects.reconnectTimeWait
18-
// queue: serviceName
17+
reconnectTimeWait: NATSReconnects.reconnectTimeWait,
18+
queue: serviceName
1919
};
2020

2121
if (nkeySeed) {

libs/keycloak-config/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './keycloak-config.module';
2+
export * from './keycloak-config.service';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { KeycloakConfigService } from './keycloak-config.service';
3+
import { CommonModule } from '@credebl/common';
4+
import { KeycloakUrlModule } from '@credebl/keycloak-url';
5+
import { ClientRegistrationModule } from '@credebl/client-registration';
6+
7+
@Module({
8+
imports: [CommonModule, KeycloakUrlModule, ClientRegistrationModule],
9+
providers: [KeycloakConfigService],
10+
exports: [KeycloakConfigService]
11+
})
12+
export class KeycloakConfigModule {}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/* eslint-disable camelcase */
2+
/* eslint-disable @typescript-eslint/explicit-function-return-type */
3+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
4+
5+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
6+
import { CommonService } from '@credebl/common';
7+
import { KeycloakUrlService } from '@credebl/keycloak-url';
8+
import { ClientRegistrationService } from '@credebl/client-registration';
9+
10+
@Injectable()
11+
export class KeycloakConfigService implements OnModuleInit {
12+
constructor(
13+
private readonly commonService: CommonService,
14+
private readonly keycloakUrlService: KeycloakUrlService,
15+
private readonly clientRegistrationService: ClientRegistrationService
16+
) {}
17+
18+
private readonly logger = new Logger('KeycloakConfigService');
19+
20+
async onModuleInit(): Promise<void> {
21+
this.logger.log('=== Keycloak Ecosystem Configuration Starting ===');
22+
try {
23+
await this.configureEcosystemAccess();
24+
this.logger.log('=== Keycloak Ecosystem Configuration Finished Successfully ===');
25+
} catch (error) {
26+
this.logger.error(`=== Keycloak Ecosystem Configuration FAILED ===`);
27+
this.logger.error(`Error: ${error.message || error}`);
28+
}
29+
}
30+
31+
async configureEcosystemAccess(): Promise<void> {
32+
this.logger.log('Fetching management token...');
33+
const token = await this.clientRegistrationService.getPlatformManagementToken();
34+
this.logger.log('Management token obtained successfully');
35+
36+
const realmName = process.env.KEYCLOAK_REALM;
37+
this.logger.log(`Target realm: ${realmName}`);
38+
39+
this.logger.log('[Step 1/2] Checking realm-level protocol mapper...');
40+
await this.ensureRealmProtocolMapper(realmName, token);
41+
42+
this.logger.log('[Step 2/2] Checking per-client protocol mappers...');
43+
await this.ensureAllClientsProtocolMapper(realmName, token);
44+
45+
this.logger.log('Ecosystem access configuration completed');
46+
}
47+
48+
private async ensureRealmProtocolMapper(realm: string, token: string): Promise<void> {
49+
this.logger.log('Looking up "profile" client scope...');
50+
const scopeId = await this.getDefaultClientScopeId(realm, token);
51+
if (!scopeId) {
52+
this.logger.warn('Default "profile" client scope not found, skipping realm-level mapper');
53+
return;
54+
}
55+
this.logger.log(`Found "profile" client scope: ${scopeId}`);
56+
57+
const mappersUrl = await this.keycloakUrlService.GetClientScopeProtocolMappersURL(realm, scopeId);
58+
this.logger.log(`Fetching existing mappers from: ${mappersUrl}`);
59+
const existingMappers = await this.commonService.httpGet(mappersUrl, this.getAuthHeader(token));
60+
61+
const mapperExists =
62+
Array.isArray(existingMappers) && existingMappers.some((m: { name: string }) => 'ecosystem_access' === m.name);
63+
64+
if (mapperExists) {
65+
this.logger.log('Realm-level "ecosystem_access" mapper already exists - SKIPPED');
66+
return;
67+
}
68+
69+
this.logger.log('Realm-level "ecosystem_access" mapper not found - CREATING...');
70+
const mapperPayload = this.buildProtocolMapperPayload('ecosystem_access');
71+
await this.commonService.httpPost(mappersUrl, mapperPayload, this.getAuthHeader(token));
72+
this.logger.log('Realm-level "ecosystem_access" mapper CREATED on "profile" client scope');
73+
}
74+
75+
private async getDefaultClientScopeId(realm: string, token: string): Promise<string | null> {
76+
const scopesUrl = await this.keycloakUrlService.GetClientScopesURL(realm);
77+
const scopes = await this.commonService.httpGet(scopesUrl, this.getAuthHeader(token));
78+
79+
if (!Array.isArray(scopes)) {
80+
this.logger.warn('Failed to fetch client scopes');
81+
return null;
82+
}
83+
84+
this.logger.log(`Found ${scopes.length} client scopes in realm`);
85+
const profileScope = scopes.find((s: { name: string }) => 'profile' === s.name);
86+
return profileScope ? profileScope.id : null;
87+
}
88+
89+
private async ensureAllClientsProtocolMapper(realm: string, token: string): Promise<void> {
90+
const clientsUrl = await this.keycloakUrlService.GetClientsURL(realm);
91+
this.logger.log(`Fetching all clients from: ${clientsUrl}`);
92+
const clients = await this.commonService.httpGet(clientsUrl, this.getAuthHeader(token));
93+
94+
if (!Array.isArray(clients)) {
95+
this.logger.warn('No clients found in realm');
96+
return;
97+
}
98+
99+
this.logger.log(`Found ${clients.length} total clients in realm`);
100+
101+
const orgClients = clients.filter(
102+
(client: { serviceAccountsEnabled: boolean; clientId: string }) =>
103+
client.serviceAccountsEnabled && !client.clientId.startsWith('realm-')
104+
);
105+
106+
this.logger.log(`Found ${orgClients.length} service-account-enabled clients to process`);
107+
108+
let created = 0;
109+
let skipped = 0;
110+
let failed = 0;
111+
112+
for (const client of orgClients) {
113+
const result = await this.ensureClientProtocolMapper(realm, client.id, client.clientId, token);
114+
if ('created' === result) {
115+
created++;
116+
} else if ('skipped' === result) {
117+
skipped++;
118+
} else {
119+
failed++;
120+
}
121+
}
122+
123+
this.logger.log(`Per-client mapper results: ${created} created, ${skipped} already existed, ${failed} failed`);
124+
}
125+
126+
private async ensureClientProtocolMapper(
127+
realm: string,
128+
clientIdpId: string,
129+
clientId: string,
130+
token: string
131+
): Promise<'created' | 'skipped' | 'failed'> {
132+
try {
133+
const mappersUrl = await this.keycloakUrlService.GetClientProtocolMappersByIdURL(realm, clientIdpId);
134+
const existingMappers = await this.commonService.httpGet(mappersUrl, this.getAuthHeader(token));
135+
136+
const mapperExists =
137+
Array.isArray(existingMappers) &&
138+
existingMappers.some((m: { name: string }) => 'ecosystem_access_mapper' === m.name);
139+
140+
if (mapperExists) {
141+
this.logger.log(` [${clientId}] ecosystem_access_mapper already exists - SKIPPED`);
142+
return 'skipped';
143+
}
144+
145+
const mapperPayload = this.buildProtocolMapperPayload('ecosystem_access_mapper');
146+
await this.commonService.httpPost(mappersUrl, mapperPayload, this.getAuthHeader(token));
147+
this.logger.log(` [${clientId}] ecosystem_access_mapper CREATED`);
148+
return 'created';
149+
} catch (error) {
150+
this.logger.warn(` [${clientId}] FAILED: ${error.message || error}`);
151+
return 'failed';
152+
}
153+
}
154+
155+
private buildProtocolMapperPayload(name: string) {
156+
return {
157+
name,
158+
protocol: 'openid-connect',
159+
protocolMapper: 'oidc-usermodel-attribute-mapper',
160+
config: {
161+
'user.attribute': 'ecosystem_access',
162+
'claim.name': 'ecosystem_access',
163+
'jsonType.label': 'JSON',
164+
'id.token.claim': 'true',
165+
'access.token.claim': 'true',
166+
'userinfo.token.claim': 'true'
167+
}
168+
};
169+
}
170+
171+
private getAuthHeader(token: string) {
172+
return { headers: { authorization: `Bearer ${token}` } };
173+
}
174+
}

libs/keycloak-url/src/keycloak-url.service.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,20 @@ export class KeycloakUrlService {
7878
async GetClientProtocolMappersURL(realm: string, clientIdpId: string): Promise<string> {
7979
return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientIdpId}/protocol-mappers/models`;
8080
}
81+
82+
async GetClientScopesURL(realm: string): Promise<string> {
83+
return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/client-scopes`;
84+
}
85+
86+
async GetClientScopeProtocolMappersURL(realm: string, scopeId: string): Promise<string> {
87+
return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/client-scopes/${scopeId}/protocol-mappers/models`;
88+
}
89+
90+
async GetClientProtocolMappersByIdURL(realm: string, clientIdpId: string): Promise<string> {
91+
return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientIdpId}/protocol-mappers/models`;
92+
}
93+
94+
async GetClientsURL(realm: string): Promise<string> {
95+
return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients?max=1000`;
96+
}
8197
}

tsconfig.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@
3838
"@credebl/enum/*": [
3939
"libs/enum/src/*"
4040
],
41+
"@credebl/keycloak-config": [
42+
"libs/keycloak-config/src"
43+
],
44+
"@credebl/keycloak-config/*": [
45+
"libs/keycloak-config/src/*"
46+
],
4147
"@credebl/keycloak-url": [
4248
"libs/keycloak-url/src"
4349
],

0 commit comments

Comments
 (0)