@platform-mesh/portal-server-lib 0.5.44 → 0.5.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,101 +1,523 @@
1
1
  import { PMAuthConfigProvider } from './auth-config-provider.js';
2
- import { HttpException } from '@nestjs/common';
3
- import {
4
- DiscoveryService,
5
- EnvAuthConfigService,
6
- } from '@openmfp/portal-server-lib';
2
+ import { IdentityProviderConfiguration } from './models/k8s.js';
3
+ import { KcpKubernetesService } from './services/kcp-k8s.service.js';
4
+ import * as domainUtils from './utils/domain.js';
5
+ import { HttpException, HttpStatus } from '@nestjs/common';
6
+ import { DiscoveryService } from '@openmfp/portal-server-lib';
7
7
  import type { Request } from 'express';
8
8
  import { mock } from 'jest-mock-extended';
9
9
 
10
10
  jest.mock('@kubernetes/client-node', () => {
11
+ const mockReadNamespacedSecret = jest.fn();
11
12
  class KubeConfig {
12
13
  loadFromDefault = jest.fn();
13
14
  loadFromFile = jest.fn();
14
15
  getCurrentCluster = jest.fn().mockReturnValue({
15
- server: 'https://k8s.example.com/base',
16
+ server: 'https://k8s.example.com',
16
17
  name: 'test-cluster',
17
18
  });
18
- makeApiClient = jest.fn();
19
+ makeApiClient = jest.fn().mockReturnValue({
20
+ readNamespacedSecret: mockReadNamespacedSecret,
21
+ });
19
22
  addUser = jest.fn();
20
23
  addContext = jest.fn();
21
24
  setCurrentContext = jest.fn();
22
25
  }
26
+ class CoreV1Api {}
23
27
  class CustomObjectsApi {}
24
- return { KubeConfig, CustomObjectsApi };
28
+ return { KubeConfig, CoreV1Api, CustomObjectsApi, mockReadNamespacedSecret };
25
29
  });
26
30
 
31
+ jest.mock('@kubernetes/client-node/dist/gen/middleware.js', () => ({
32
+ PromiseMiddlewareWrapper: class {},
33
+ }));
34
+
27
35
  describe('PMAuthConfigProvider', () => {
28
36
  let provider: PMAuthConfigProvider;
29
37
  let discoveryService: jest.Mocked<DiscoveryService>;
30
- let envAuthConfigService: jest.Mocked<EnvAuthConfigService>;
38
+ let kcpKubernetesService: jest.Mocked<KcpKubernetesService>;
39
+ let mockRequest: Request;
40
+
41
+ const mockOidcDiscovery = {
42
+ authorization_endpoint: 'https://auth.example.com/authorize',
43
+ token_endpoint: 'https://auth.example.com/token',
44
+ issuer: 'https://auth.example.com',
45
+ end_session_endpoint: 'https://auth.example.com/logout',
46
+ };
31
47
 
32
48
  beforeEach(() => {
33
49
  discoveryService = mock<DiscoveryService>();
34
- envAuthConfigService = mock<EnvAuthConfigService>();
35
- provider = new PMAuthConfigProvider(discoveryService);
36
- jest.resetModules();
50
+ kcpKubernetesService = mock<KcpKubernetesService>();
51
+ provider = new PMAuthConfigProvider(discoveryService, kcpKubernetesService);
52
+
53
+ mockRequest = { hostname: 'org1.example.com' } as Request;
54
+
37
55
  process.env = {
38
- AUTH_SERVER_URL_DEFAULT: 'authUrl',
39
- TOKEN_URL_DEFAULT: 'tokenUrl',
40
56
  BASE_DOMAINS_DEFAULT: 'example.com',
41
- OIDC_CLIENT_ID_DEFAULT: 'client123',
42
- OIDC_CLIENT_SECRET_DEFAULT: 'secret123',
57
+ AUTH_SERVER_URL_DEFAULT: 'https://default-auth.com/authorize',
58
+ TOKEN_URL_DEFAULT: 'https://default-auth.com/token',
43
59
  };
44
- provider['getClientSecret'] = jest.fn().mockResolvedValue('secret');
60
+
61
+ jest
62
+ .spyOn(domainUtils, 'getDiscoveryEndpoint')
63
+ .mockReturnValue(
64
+ 'https://oidc.example.com/.well-known/openid-configuration',
65
+ );
66
+ jest.spyOn(domainUtils, 'getOrganization').mockReturnValue('org1');
67
+
68
+ discoveryService.getOIDC.mockResolvedValue(mockOidcDiscovery);
45
69
  });
46
70
 
47
- it('should delegate to EnvAuthConfigService if available', async () => {
48
- const req = { hostname: 'foo.example.com' } as Request;
49
- const expected = {
50
- idpName: 'idp',
51
- baseDomain: 'example.com',
52
- oauthServerUrl: 'url',
53
- oauthTokenUrl: 'token',
54
- clientId: 'cid',
55
- clientSecret: 'secret',
56
- oidcIssuerUrl: 'issuer',
57
- };
58
- envAuthConfigService.getAuthConfig.mockResolvedValue(expected);
71
+ afterEach(() => {
72
+ jest.clearAllMocks();
73
+ });
74
+
75
+ describe('getAuthConfig', () => {
76
+ it('should return auth config for regular organization', async () => {
77
+ const mockIdpConfig: IdentityProviderConfiguration = {
78
+ status: {
79
+ managedClients: {
80
+ org1: {
81
+ clientId: 'client-org1',
82
+ },
83
+ },
84
+ },
85
+ } as IdentityProviderConfiguration;
86
+
87
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
88
+ mockIdpConfig,
89
+ );
90
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
91
+
92
+ const result = await provider.getAuthConfig(mockRequest);
93
+
94
+ expect(result).toEqual({
95
+ idpName: 'org1',
96
+ baseDomain: 'example.com',
97
+ clientId: 'client-org1',
98
+ clientSecret: 'secret-org1',
99
+ oauthServerUrl: 'https://auth.example.com/authorize',
100
+ oauthTokenUrl: 'https://auth.example.com/token',
101
+ oidcIssuerUrl: 'https://auth.example.com',
102
+ endSessionUrl: 'https://auth.example.com/logout',
103
+ });
104
+ });
105
+
106
+ it('should handle welcome organization', async () => {
107
+ jest.spyOn(domainUtils, 'getOrganization').mockReturnValue('welcome');
108
+
109
+ const { mockReadNamespacedSecret } = require('@kubernetes/client-node');
110
+ mockReadNamespacedSecret.mockResolvedValue({
111
+ data: {
112
+ 'attribute.client_secret':
113
+ Buffer.from('welcome-secret').toString('base64'),
114
+ },
115
+ });
116
+
117
+ const result = await provider.getAuthConfig(mockRequest);
118
+
119
+ expect(result.clientId).toBe('welcome');
120
+ expect(result.clientSecret).toBe('welcome-secret');
121
+ expect(
122
+ kcpKubernetesService.listClusterCustomObject,
123
+ ).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it('should fall back to default auth URLs when OIDC discovery fails', async () => {
127
+ discoveryService.getOIDC.mockResolvedValue(null);
128
+
129
+ const mockIdpConfig: IdentityProviderConfiguration = {
130
+ status: {
131
+ managedClients: {
132
+ org1: {
133
+ clientId: 'client-org1',
134
+ },
135
+ },
136
+ },
137
+ } as IdentityProviderConfiguration;
138
+
139
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
140
+ mockIdpConfig,
141
+ );
142
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
143
+
144
+ const result = await provider.getAuthConfig(mockRequest);
145
+
146
+ expect(result.oauthServerUrl).toBe('https://default-auth.com/authorize');
147
+ expect(result.oauthTokenUrl).toBe('https://default-auth.com/token');
148
+ expect(result.oidcIssuerUrl).toBeUndefined();
149
+ expect(result.endSessionUrl).toBeUndefined();
150
+ });
151
+
152
+ it('should throw HttpException when oauthServerUrl is missing', async () => {
153
+ discoveryService.getOIDC.mockResolvedValue(null);
154
+ process.env.AUTH_SERVER_URL_DEFAULT = '';
155
+
156
+ const mockIdpConfig: IdentityProviderConfiguration = {
157
+ status: {
158
+ managedClients: {
159
+ org1: {
160
+ clientId: 'client-org1',
161
+ },
162
+ },
163
+ },
164
+ } as IdentityProviderConfiguration;
165
+
166
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
167
+ mockIdpConfig,
168
+ );
169
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
170
+
171
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow(
172
+ HttpException,
173
+ );
174
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toMatchObject({
175
+ status: HttpStatus.NOT_FOUND,
176
+ });
177
+ });
178
+
179
+ it('should throw HttpException when oauthTokenUrl is missing', async () => {
180
+ discoveryService.getOIDC.mockResolvedValue(null);
181
+ process.env.TOKEN_URL_DEFAULT = '';
182
+
183
+ const mockIdpConfig: IdentityProviderConfiguration = {
184
+ status: {
185
+ managedClients: {
186
+ org1: {
187
+ clientId: 'client-org1',
188
+ },
189
+ },
190
+ },
191
+ } as IdentityProviderConfiguration;
192
+
193
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
194
+ mockIdpConfig,
195
+ );
196
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
197
+
198
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow(
199
+ HttpException,
200
+ );
201
+ });
202
+
203
+ it('should throw HttpException when clientId is missing', async () => {
204
+ const mockIdpConfig: IdentityProviderConfiguration = {
205
+ status: {
206
+ managedClients: {
207
+ org1: {
208
+ clientId: '',
209
+ },
210
+ },
211
+ },
212
+ } as IdentityProviderConfiguration;
59
213
 
60
- const result = await provider.getAuthConfig(req);
214
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
215
+ mockIdpConfig,
216
+ );
217
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
61
218
 
62
- expect(result).toEqual({
63
- baseDomain: 'example.com',
64
- clientId: 'foo',
65
- clientSecret: 'secret',
66
- idpName: 'foo',
67
- oauthServerUrl: 'authUrl',
68
- oauthTokenUrl: 'tokenUrl',
219
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow(
220
+ HttpException,
221
+ );
222
+ });
223
+
224
+ it('should throw HttpException when clientSecret is missing', async () => {
225
+ const mockIdpConfig: IdentityProviderConfiguration = {
226
+ status: {
227
+ managedClients: {
228
+ org1: {
229
+ clientId: 'client-org1',
230
+ },
231
+ },
232
+ },
233
+ } as IdentityProviderConfiguration;
234
+
235
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
236
+ mockIdpConfig,
237
+ );
238
+ kcpKubernetesService.getClientSecret.mockResolvedValue('');
239
+
240
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow(
241
+ HttpException,
242
+ );
243
+ });
244
+
245
+ it('should include error details in HttpException', async () => {
246
+ discoveryService.getOIDC.mockResolvedValue(null);
247
+ process.env.AUTH_SERVER_URL_DEFAULT = '';
248
+
249
+ const mockIdpConfig: IdentityProviderConfiguration = {
250
+ status: {
251
+ managedClients: {
252
+ org1: {
253
+ clientId: 'client-org1',
254
+ },
255
+ },
256
+ },
257
+ } as IdentityProviderConfiguration;
258
+
259
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
260
+ mockIdpConfig,
261
+ );
262
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
263
+
264
+ try {
265
+ await provider.getAuthConfig(mockRequest);
266
+ fail('Should have thrown HttpException');
267
+ } catch (error) {
268
+ expect(error).toBeInstanceOf(HttpException);
269
+ const response = (error as HttpException).getResponse() as any;
270
+ expect(response.message).toBe('Default auth configuration incomplete.');
271
+ expect(response.error).toContain("oauthServerUrl: ''");
272
+ expect(response.error).toContain('has client secret: true');
273
+ }
274
+ });
275
+
276
+ it('should call getDiscoveryEndpoint with request', async () => {
277
+ const mockIdpConfig: IdentityProviderConfiguration = {
278
+ status: {
279
+ managedClients: {
280
+ org1: {
281
+ clientId: 'client-org1',
282
+ },
283
+ },
284
+ },
285
+ } as IdentityProviderConfiguration;
286
+
287
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
288
+ mockIdpConfig,
289
+ );
290
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
291
+
292
+ await provider.getAuthConfig(mockRequest);
293
+
294
+ expect(domainUtils.getDiscoveryEndpoint).toHaveBeenCalledWith(
295
+ mockRequest,
296
+ );
297
+ });
298
+
299
+ it('should call getOrganization with request', async () => {
300
+ const mockIdpConfig: IdentityProviderConfiguration = {
301
+ status: {
302
+ managedClients: {
303
+ org1: {
304
+ clientId: 'client-org1',
305
+ },
306
+ },
307
+ },
308
+ } as IdentityProviderConfiguration;
309
+
310
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
311
+ mockIdpConfig,
312
+ );
313
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
314
+
315
+ await provider.getAuthConfig(mockRequest);
316
+
317
+ expect(domainUtils.getOrganization).toHaveBeenCalledWith(mockRequest);
318
+ });
319
+
320
+ it('should call discoveryService with OIDC URL', async () => {
321
+ const mockIdpConfig: IdentityProviderConfiguration = {
322
+ status: {
323
+ managedClients: {
324
+ org1: {
325
+ clientId: 'client-org1',
326
+ },
327
+ },
328
+ },
329
+ } as IdentityProviderConfiguration;
330
+
331
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
332
+ mockIdpConfig,
333
+ );
334
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
335
+
336
+ await provider.getAuthConfig(mockRequest);
337
+
338
+ expect(discoveryService.getOIDC).toHaveBeenCalledWith(
339
+ 'https://oidc.example.com/.well-known/openid-configuration',
340
+ );
69
341
  });
70
342
  });
71
343
 
72
- it('should fall back to default configuration if EnvAuthConfigService throws', async () => {
73
- const req = { hostname: 'foo.example.com' } as Request;
74
- envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail'));
75
- discoveryService.getOIDC.mockResolvedValue({
76
- authorization_endpoint: 'authUrl',
77
- token_endpoint: 'tokenUrl',
78
- issuer: 'issuer',
79
- end_session_endpoint: 'endSessionUrl',
344
+ describe('readClientId', () => {
345
+ it('should read client ID from identity provider configuration', async () => {
346
+ const mockIdpConfig: IdentityProviderConfiguration = {
347
+ status: {
348
+ managedClients: {
349
+ org1: {
350
+ clientId: 'client-org1',
351
+ },
352
+ },
353
+ },
354
+ } as IdentityProviderConfiguration;
355
+
356
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
357
+ mockIdpConfig,
358
+ );
359
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
360
+
361
+ await provider.getAuthConfig(mockRequest);
362
+
363
+ expect(kcpKubernetesService.listClusterCustomObject).toHaveBeenCalledWith(
364
+ {
365
+ group: 'core.platform-mesh.io',
366
+ version: 'v1alpha1',
367
+ plural: 'identityproviderconfigurations',
368
+ name: 'org1',
369
+ },
370
+ {
371
+ organization: 'org1',
372
+ },
373
+ );
80
374
  });
81
375
 
82
- const result = await provider.getAuthConfig(req);
376
+ it('should handle missing managedClients', async () => {
377
+ const mockIdpConfig: IdentityProviderConfiguration = {
378
+ status: {
379
+ managedClients: {},
380
+ },
381
+ } as IdentityProviderConfiguration;
83
382
 
84
- expect(result).toMatchObject({
85
- baseDomain: 'example.com',
86
- oauthServerUrl: 'authUrl',
87
- oauthTokenUrl: 'tokenUrl',
88
- clientId: 'foo',
89
- clientSecret: 'secret',
383
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
384
+ mockIdpConfig,
385
+ );
386
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
387
+
388
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow();
90
389
  });
91
390
  });
92
391
 
93
- it('should throw if default configuration incomplete', async () => {
94
- const req = { hostname: 'foo.example.com' } as Request;
95
- envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail'));
96
- discoveryService.getOIDC.mockResolvedValue(null);
97
- process.env = {};
392
+ describe('getWelcomeClientSecret', () => {
393
+ it('should read welcome client secret from Kubernetes secret', async () => {
394
+ jest.spyOn(domainUtils, 'getOrganization').mockReturnValue('welcome');
395
+
396
+ const { mockReadNamespacedSecret } = require('@kubernetes/client-node');
397
+ const secretValue = 'my-welcome-secret';
398
+ mockReadNamespacedSecret.mockResolvedValue({
399
+ data: {
400
+ 'attribute.client_secret':
401
+ Buffer.from(secretValue).toString('base64'),
402
+ },
403
+ });
404
+
405
+ const result = await provider.getAuthConfig(mockRequest);
98
406
 
99
- await expect(provider.getAuthConfig(req)).rejects.toThrow(HttpException);
407
+ expect(mockReadNamespacedSecret).toHaveBeenCalledWith({
408
+ namespace: 'platform-mesh-system',
409
+ name: 'portal-client-secret-welcome',
410
+ });
411
+ expect(result.clientSecret).toBe(secretValue);
412
+ });
413
+
414
+ it('should decode base64 secret correctly', async () => {
415
+ jest.spyOn(domainUtils, 'getOrganization').mockReturnValue('welcome');
416
+
417
+ const { mockReadNamespacedSecret } = require('@kubernetes/client-node');
418
+ const secretValue = 'special-chars-@#$%';
419
+ mockReadNamespacedSecret.mockResolvedValue({
420
+ data: {
421
+ 'attribute.client_secret':
422
+ Buffer.from(secretValue).toString('base64'),
423
+ },
424
+ });
425
+
426
+ const result = await provider.getAuthConfig(mockRequest);
427
+
428
+ expect(result.clientSecret).toBe(secretValue);
429
+ });
430
+
431
+ it('should throw error when secret read fails', async () => {
432
+ jest.spyOn(domainUtils, 'getOrganization').mockReturnValue('welcome');
433
+
434
+ const { mockReadNamespacedSecret } = require('@kubernetes/client-node');
435
+ const error = new Error('Secret not found');
436
+ mockReadNamespacedSecret.mockRejectedValue(error);
437
+
438
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
439
+
440
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow(
441
+ 'Secret not found',
442
+ );
443
+ expect(consoleSpy).toHaveBeenCalled();
444
+
445
+ consoleSpy.mockRestore();
446
+ });
447
+
448
+ it('should log error with response body if available', async () => {
449
+ jest.spyOn(domainUtils, 'getOrganization').mockReturnValue('welcome');
450
+
451
+ const { mockReadNamespacedSecret } = require('@kubernetes/client-node');
452
+ const error: any = new Error('Secret not found');
453
+ error.response = { body: { message: 'Not found in namespace' } };
454
+ mockReadNamespacedSecret.mockRejectedValue(error);
455
+
456
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
457
+
458
+ await expect(provider.getAuthConfig(mockRequest)).rejects.toThrow();
459
+ expect(consoleSpy).toHaveBeenCalledWith(
460
+ 'Failed to fetch secret portal-client-secret-welcome:',
461
+ { message: 'Not found in namespace' },
462
+ );
463
+
464
+ consoleSpy.mockRestore();
465
+ });
466
+ });
467
+
468
+ describe('edge cases', () => {
469
+ it('should handle undefined OIDC discovery endpoints', async () => {
470
+ discoveryService.getOIDC.mockResolvedValue({
471
+ authorization_endpoint: undefined,
472
+ token_endpoint: undefined,
473
+ issuer: undefined,
474
+ end_session_endpoint: undefined,
475
+ } as any);
476
+
477
+ const mockIdpConfig: IdentityProviderConfiguration = {
478
+ status: {
479
+ managedClients: {
480
+ org1: {
481
+ clientId: 'client-org1',
482
+ },
483
+ },
484
+ },
485
+ } as IdentityProviderConfiguration;
486
+
487
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
488
+ mockIdpConfig,
489
+ );
490
+ kcpKubernetesService.getClientSecret.mockResolvedValue('secret-org1');
491
+
492
+ const result = await provider.getAuthConfig(mockRequest);
493
+
494
+ expect(result.oauthServerUrl).toBe('https://default-auth.com/authorize');
495
+ expect(result.oauthTokenUrl).toBe('https://default-auth.com/token');
496
+ });
497
+
498
+ it('should handle null clientSecret in error message', async () => {
499
+ const mockIdpConfig: IdentityProviderConfiguration = {
500
+ status: {
501
+ managedClients: {
502
+ org1: {
503
+ clientId: 'client-org1',
504
+ },
505
+ },
506
+ },
507
+ } as IdentityProviderConfiguration;
508
+
509
+ kcpKubernetesService.listClusterCustomObject.mockResolvedValue(
510
+ mockIdpConfig,
511
+ );
512
+ kcpKubernetesService.getClientSecret.mockResolvedValue(null as any);
513
+
514
+ try {
515
+ await provider.getAuthConfig(mockRequest);
516
+ fail('Should have thrown');
517
+ } catch (error) {
518
+ const response = (error as HttpException).getResponse() as any;
519
+ expect(response.error).toContain('has client secret: false');
520
+ }
521
+ });
100
522
  });
101
523
  });
@@ -1,3 +1,8 @@
1
+ import {
2
+ IdentityProviderConfiguration,
3
+ K8sResourceDescriptor,
4
+ } from './models/k8s.js';
5
+ import { KcpKubernetesService } from './services/kcp-k8s.service.js';
1
6
  import { getDiscoveryEndpoint, getOrganization } from './utils/domain.js';
2
7
  import { CoreV1Api, KubeConfig } from '@kubernetes/client-node';
3
8
  import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
@@ -10,20 +15,23 @@ import type { Request } from 'express';
10
15
 
11
16
  @Injectable()
12
17
  export class PMAuthConfigProvider implements AuthConfigService {
13
- private k8sApi: CoreV1Api;
14
-
15
- constructor(private discoveryService: DiscoveryService) {
16
- const kc = new KubeConfig();
17
- kc.loadFromDefault();
18
- this.k8sApi = kc.makeApiClient(CoreV1Api);
19
- }
18
+ constructor(
19
+ private discoveryService: DiscoveryService,
20
+ private kcpKubernetesService: KcpKubernetesService,
21
+ ) {}
20
22
 
21
23
  async getAuthConfig(request: Request): Promise<ServerAuthVariables> {
22
24
  const oidcUrl = getDiscoveryEndpoint(request);
23
- const clientId = getOrganization(request);
25
+ const org = getOrganization(request);
26
+
27
+ const clientId =
28
+ org === 'welcome' ? 'welcome' : await this.readClientId(org);
29
+ const clientSecret =
30
+ org === 'welcome'
31
+ ? await this.getWelcomeClientSecret(org)
32
+ : await this.kcpKubernetesService.getClientSecret(org);
24
33
 
25
34
  const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
26
- const clientSecret = await this.getClientSecret(clientId);
27
35
  const oidc = await this.discoveryService.getOIDC(oidcUrl);
28
36
  const oauthServerUrl =
29
37
  oidc?.authorization_endpoint ?? process.env['AUTH_SERVER_URL_DEFAULT'];
@@ -45,7 +53,7 @@ export class PMAuthConfigProvider implements AuthConfigService {
45
53
  }
46
54
 
47
55
  return {
48
- idpName: clientId,
56
+ idpName: org,
49
57
  baseDomain,
50
58
  clientId,
51
59
  clientSecret,
@@ -56,12 +64,33 @@ export class PMAuthConfigProvider implements AuthConfigService {
56
64
  };
57
65
  }
58
66
 
59
- private async getClientSecret(orgName: string) {
67
+ private async readClientId(orgName: string): Promise<string> {
68
+ const k8sResourceDescriptor: K8sResourceDescriptor = {
69
+ group: 'core.platform-mesh.io',
70
+ version: 'v1alpha1',
71
+ plural: 'identityproviderconfigurations',
72
+ name: orgName,
73
+ };
74
+
75
+ const result: IdentityProviderConfiguration =
76
+ await this.kcpKubernetesService.listClusterCustomObject(
77
+ k8sResourceDescriptor,
78
+ {
79
+ organization: orgName,
80
+ },
81
+ );
82
+ return result.status.managedClients[orgName].clientId;
83
+ }
84
+
85
+ private async getWelcomeClientSecret(orgName: string) {
60
86
  const secretName = `portal-client-secret-${orgName}`;
61
87
  const namespace = 'platform-mesh-system';
62
88
 
89
+ const kc = new KubeConfig();
90
+ kc.loadFromDefault();
91
+ const k8sApi = kc.makeApiClient(CoreV1Api);
63
92
  try {
64
- const res = await this.k8sApi.readNamespacedSecret({
93
+ const res = await k8sApi.readNamespacedSecret({
65
94
  namespace,
66
95
  name: secretName,
67
96
  });
@@ -0,0 +1,27 @@
1
+ export interface K8sResourceDescriptor {
2
+ group: string;
3
+ version: string;
4
+ plural: string;
5
+ name?: string;
6
+ namespace?: string;
7
+ labelSelector?: string;
8
+ }
9
+
10
+ export interface K8sRequestContext extends Record<string, any> {
11
+ organization: string;
12
+ 'core_platform-mesh_io_account'?: string;
13
+ }
14
+
15
+ export interface IdentityProviderConfiguration {
16
+ status: {
17
+ managedClients: {
18
+ [key: string]: {
19
+ clientId: string;
20
+ secretRef?: {
21
+ name: string;
22
+ namespace: string;
23
+ };
24
+ };
25
+ };
26
+ };
27
+ }