From cc08b4ef301c5eb40a672f509a8126fe53adebfd Mon Sep 17 00:00:00 2001 From: sathish-palanivelu Date: Wed, 30 Sep 2026 11:46:23 +0100 Subject: [PATCH] feat: add manage rights to fun for queues --- .changeset/sb-queue-auth-manage-claim.md | 6 ++++++ .../azure/src/construct/event-handler/main.ts | 18 +++++++++++------- .../azure/src/construct/event-handler/types.ts | 2 +- .../test/constructs/event-handler.test.ts | 4 ++-- 4 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 .changeset/sb-queue-auth-manage-claim.md diff --git a/.changeset/sb-queue-auth-manage-claim.md b/.changeset/sb-queue-auth-manage-claim.md new file mode 100644 index 000000000..bf45adf44 --- /dev/null +++ b/.changeset/sb-queue-auth-manage-claim.md @@ -0,0 +1,6 @@ +--- +'@gradientedge/cdk-utils-azure': patch +'@gradientedge/cdk-utils': patch +--- + +Grant `Manage` on the per-queue `listen-send` authorization rule created by `AzureEventHandler`. The Functions scale controller calls the Service Bus management API to derive queue-length metrics, which requires `Manage`/`EntityRead`; without it the host logged a 401 on every invocation and fell back to first-message-enqueued-time based scaling. The rule name is unchanged so the rights update in place without rotating the SAS keys behind `EVENT_INGEST_SERVICE_BUS`. diff --git a/packages/azure/src/construct/event-handler/main.ts b/packages/azure/src/construct/event-handler/main.ts index db3dbd08a..53a349ce5 100644 --- a/packages/azure/src/construct/event-handler/main.ts +++ b/packages/azure/src/construct/event-handler/main.ts @@ -46,7 +46,7 @@ const requiredInput = (value: Input, description: string): O * ## Authorization and the `EVENT_INGEST_SERVICE_BUS` connection string * * When the construct owns the queue (`queue.useExisting=false`), it provisions a per-queue - * authorization rule named `listen-send` (scoped to the queue) with `Listen + Send` rights, and the function app's + * authorization rule named `listen-send` (scoped to the queue) with `Listen + Send + Manage` rights, and the function app's * `EVENT_INGEST_SERVICE_BUS` connection string is sourced from that rule. This avoids granting the * function app access to sibling queues when the namespace is shared. * @@ -258,16 +258,18 @@ export class AzureEventHandler extends AzureFunctionApp { } /** - * @summary Provision a per-queue Listen+Send authorization rule. + * @summary Provision a per-queue Listen+Send+Manage authorization rule. * * Skipped when the construct does not own the queue (`queue.useExisting=true`) — in that case * the producer/owner of the queue is responsible for its auth rules and the function app's * connection string falls back to the namespace-level root rule in {@link createFunctionAppSiteConfig}. * * Replaces the previous reliance on `RootManageSharedAccessKey`, which grants Listen+Send+Manage - * on every queue in the namespace. The new rule narrows the scope to this one queue while keeping - * Listen+Send so existing function code can both consume and publish through it (Manage is - * intentionally dropped — a runtime app should not create or delete queues). + * on every queue in the namespace. This rule narrows that scope to this one queue. + * + * Manage is included because the Functions scale controller calls the Service Bus management API + * to read queue-length metrics; that requires Manage/EntityRead. Without it the host logs a 401 + * per invocation and silently degrades to first-message-enqueued-time based scaling. */ protected createServiceBusQueueAuthorizationRule() { const useExistingFlags = this.resolveServiceBusUseExisting() @@ -279,7 +281,9 @@ export class AzureEventHandler extends AzureFunctionApp { // Azure caps `authorizationRuleName` at 50 chars. The rule's scope is the queue itself // (`…/namespaces//queues//authorizationRules/`), so a literal name is - // unambiguous and avoids hitting the cap on long stack ids. + // unambiguous and avoids hitting the cap on long stack ids. The name is kept as `listen-send` + // even though Manage is granted: it is immutable, so renaming would replace the rule and + // rotate the SAS keys behind every consumer's connection string. this.serviceBus.queueAuthorizationRule = this.serviceBusManager.createServiceBusQueueAuthorizationRule( this.id, this, @@ -288,7 +292,7 @@ export class AzureEventHandler extends AzureFunctionApp { namespaceName: this.serviceBus.namespace.name, queueName: this.serviceBus.queue.name, resourceGroupName: namespaceResourceGroupName, - rights: [AccessRights.Listen, AccessRights.Send], + rights: [AccessRights.Listen, AccessRights.Send, AccessRights.Manage], } ) diff --git a/packages/azure/src/construct/event-handler/types.ts b/packages/azure/src/construct/event-handler/types.ts index d97a3d7f8..ff16069a5 100644 --- a/packages/azure/src/construct/event-handler/types.ts +++ b/packages/azure/src/construct/event-handler/types.ts @@ -94,7 +94,7 @@ export interface EventHandlerServiceBus { /** The provisioned or resolved Service Bus queue */ queue: Queue | Output /** - * Per-queue authorization rule (Listen+Send) used to build the function app's + * Per-queue authorization rule (Listen+Send+Manage) used to build the function app's * `EVENT_INGEST_SERVICE_BUS` connection string. Provisioned only when the * construct owns the queue (`queue.useExisting=false`); undefined when the * queue is external and the connection string falls back to the namespace-level rule. diff --git a/packages/azure/test/constructs/event-handler.test.ts b/packages/azure/test/constructs/event-handler.test.ts index 2b795c8be..a5bfefc10 100644 --- a/packages/azure/test/constructs/event-handler.test.ts +++ b/packages/azure/test/constructs/event-handler.test.ts @@ -822,7 +822,7 @@ describe('TestAzureEventHandlerSharedNamespace', () => { ) }) - test('provisions a per-queue Listen+Send authorization rule', async () => { + test('provisions a per-queue Listen+Send+Manage authorization rule', async () => { expect(stackSharedNamespace.construct.serviceBus.queueAuthorizationRule).toBeDefined() await outputToPromise( pulumi @@ -832,7 +832,7 @@ describe('TestAzureEventHandlerSharedNamespace', () => { ]) .apply(([name, rights]) => { expect(name).toContain('listen-send') - expect(rights).toEqual(['Listen', 'Send']) + expect(rights).toEqual(['Listen', 'Send', 'Manage']) }) ) })