From 865c8f254638241f5645bc274fe94eff9bdc6575 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 28 Apr 2026 16:57:54 +0000 Subject: [PATCH] fix: allow coreflow deployment input reads --- test/test.apiclient.ts | 48 ++++++++++++++++++++++ ts/manager.auth/classes.authmanager.ts | 16 ++++++++ ts/manager.image/classes.imagemanager.ts | 2 +- ts/manager.secret/classes.secretmanager.ts | 12 ++++++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/test/test.apiclient.ts b/test/test.apiclient.ts index c7cde04..662dbde 100644 --- a/test/test.apiclient.ts +++ b/test/test.apiclient.ts @@ -246,6 +246,54 @@ tap.test('should push service config updates to connected coreflows', async (too } }); +tap.test('should allow cluster coreflows to read deployment inputs', async () => { + const cluster = await testClient.cluster.createCluster('Registry Coreflow Read Test Cluster'); + const persistedCluster = await testCloudly.clusterManager.getConfigBy_ConfigID(cluster.id); + const clusterUser = await testCloudly.authManager.CUser.getInstance({ + id: persistedCluster.data.userId, + }); + const clusterToken = clusterUser.data.tokens?.[0]?.token; + expect(clusterToken).toBeTruthy(); + + const image = await testClient.image.createImage({ + name: 'Registry Coreflow Read Test Image', + description: 'Image used by the coreflow read test', + }); + const secretBundle = await testClient.secretbundle.createSecretBundle({ + name: 'Registry Coreflow Read Test Secret Bundle', + description: 'Secret bundle used by the coreflow read test', + type: 'service', + includedSecretGroupIds: [], + includedTags: [], + imageClaims: [], + authorizations: [ + { + environment: 'production', + secretAccessKey: 'registry-coreflow-read-test', + }, + ], + }); + + const coreflowClient = new cloudlyApiClient.CloudlyApiClient({ + registerAs: 'coreflow', + cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`, + }); + + try { + await coreflowClient.start(); + await coreflowClient.getIdentityByToken(clusterToken!, { + statefullIdentity: true, + tagConnection: true, + }); + const clusterImage = await coreflowClient.image.getImageById(image.id); + const clusterSecretBundle = await coreflowClient.secretbundle.getSecretBundleById(secretBundle.id); + expect(clusterImage.id).toEqual(image.id); + expect(clusterSecretBundle.id).toEqual(secretBundle.id); + } finally { + await coreflowClient.stop(); + } +}); + tap.test('should expose platform desired state', async () => { const capabilitiesResponse = await testClient.platform.getPlatformCapabilities(); expect(capabilitiesResponse.capabilities.find((capability) => capability.id === 'database')).toBeTruthy(); diff --git a/ts/manager.auth/classes.authmanager.ts b/ts/manager.auth/classes.authmanager.ts index 00097a2..a2c4b8b 100644 --- a/ts/manager.auth/classes.authmanager.ts +++ b/ts/manager.auth/classes.authmanager.ts @@ -172,4 +172,20 @@ export class CloudlyAuthManager { name: 'adminIdentityGuard', }, ); + + public adminOrClusterIdentityGuard = new plugins.smartguard.Guard<{ + identity: plugins.servezoneInterfaces.data.IIdentity; + }>( + async (dataArg) => { + await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]); + const jwt = dataArg.identity.jwt; + const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt); + const user = await this.CUser.getInstance({ id: jwtData.userId }); + return user.data.role === 'admin' || user.data.role === 'cluster'; + }, + { + failedHint: 'user is not admin or cluster.', + name: 'adminOrClusterIdentityGuard', + }, + ); } diff --git a/ts/manager.image/classes.imagemanager.ts b/ts/manager.image/classes.imagemanager.ts index 9094ce8..507b8cf 100644 --- a/ts/manager.image/classes.imagemanager.ts +++ b/ts/manager.image/classes.imagemanager.ts @@ -41,7 +41,7 @@ export class ImageManager { this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler('getImage', async (reqArg, toolsArg) => { - await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg); + await toolsArg.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], reqArg); const image = await this.CImage.getInstance({ id: reqArg.imageId, }); diff --git a/ts/manager.secret/classes.secretmanager.ts b/ts/manager.secret/classes.secretmanager.ts index cbf297c..90ac533 100644 --- a/ts/manager.secret/classes.secretmanager.ts +++ b/ts/manager.secret/classes.secretmanager.ts @@ -54,6 +54,18 @@ export class CloudlySecretManager { ), ); + this.typedrouter.addTypedHandler( + new plugins.typedrequest.TypedHandler('getSecretBundleById', async (dataArg, toolsArg) => { + await toolsArg.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], dataArg); + const secretBundle = await SecretBundle.getInstance({ + id: dataArg.secretBundleId, + }); + return { + secretBundle: await secretBundle.createSavableObject(), + }; + }), + ); + this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler('createSecretBundle', async (dataArg) => { const secretBundle = new SecretBundle();