Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/fix-build-source-user-authorizations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@slack/bolt": patch
---

Fix `context.userId` being `undefined` for events whose payload does not carry a user field directly, by sourcing the user ID from the request's `authorizations` array in `buildSource`, matching how `teamId` and `enterpriseId` are already resolved.
16 changes: 15 additions & 1 deletion src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,21 @@ function buildSource<IsEnterpriseInstall extends boolean>(
const userId: string | undefined = (() => {
if (type === IncomingEventType.Event) {
// NOTE: no type system backed exhaustiveness check within this incoming event type
const { event } = body as SlackEventMiddlewareArgs['body'];
const bodyAsEvent = body as SlackEventMiddlewareArgs['body'];
// The authorizations array names the installing user this event was
// delivered for. Event payload fields like `event.user` reference the
// user that *triggered* the event, who may never have installed the
// app (e.g. the invitee in `member_joined_channel`), which sends
// `authorize`/`fetchInstallation` looking up the wrong installation.
// Mirrors the teamId/enterpriseId extraction above. See #2271.
if (
Array.isArray(bodyAsEvent.authorizations) &&
bodyAsEvent.authorizations[0] !== undefined &&
bodyAsEvent.authorizations[0].user_id !== null
) {
return bodyAsEvent.authorizations[0].user_id;
}
const { event } = bodyAsEvent;
if ('user' in event) {
if (typeof event.user === 'string') {
return event.user;
Expand Down
95 changes: 95 additions & 0 deletions test/unit/App/build-source.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import assert from 'node:assert';
import sinon, { type SinonSpy } from 'sinon';
import type App from '../../../src/App';
import {
createDummyAppMentionEventMiddlewareArgs,
createFakeLogger,
FakeReceiver,
importApp,
mergeOverrides,
noopMiddleware,
type Override,
withConversationContext,
withMemoryStore,
withNoopAppMetadata,
withNoopWebClient,
} from '../helpers';

function buildOverrides(secondOverrides: Override[]): Override {
return mergeOverrides(
withNoopAppMetadata(),
withNoopWebClient(),
...secondOverrides,
withMemoryStore(sinon.fake()),
withConversationContext(sinon.fake.returns(noopMiddleware)),
);
}

describe('App authorize source (buildSource)', () => {
let fakeReceiver: FakeReceiver;
let fakeHandler: SinonSpy;
let fakeAck: SinonSpy;
let fakeAuthorize: SinonSpy;
let MockApp: Awaited<ReturnType<typeof importApp>>;
let app: App;

beforeEach(async () => {
fakeReceiver = new FakeReceiver();
fakeHandler = sinon.fake();
fakeAck = sinon.fake();
fakeAuthorize = sinon.fake.resolves({ botToken: '', botId: '' });
MockApp = importApp(buildOverrides([]));
app = new MockApp({
logger: createFakeLogger(),
receiver: fakeReceiver,
authorize: fakeAuthorize,
});
});

it('should prefer the authorizations array user over the event user for events', async () => {
// The event `user` is whoever triggered the event (e.g. the invitee in
// `member_joined_channel`) and may never have installed the app; the
// authorizations array names the installing user the event was
// delivered for. authorize/fetchInstallation must be keyed on the
// latter. See #2271.
app.event('app_mention', fakeHandler);
await fakeReceiver.sendEvent({
...createDummyAppMentionEventMiddlewareArgs(undefined, {
authorizations: [
{
enterprise_id: null,
team_id: 'T1234',
user_id: 'U-installer',
is_bot: false,
is_enterprise_install: false,
},
],
}),
ack: fakeAck,
});
sinon.assert.calledOnce(fakeAuthorize);
const source = fakeAuthorize.getCall(0).args[0];
assert.strictEqual(source.userId, 'U-installer');
});

it('should fall back to the event user when no authorizations array is present', async () => {
app.event('app_mention', fakeHandler);
const args = createDummyAppMentionEventMiddlewareArgs({
event: {
type: 'app_mention',
text: 'hi',
user: 'U-event-user',
channel: 'C1234',
ts: '1234.56',
event_ts: '1234.56',
},
});
await fakeReceiver.sendEvent({
...args,
ack: fakeAck,
});
sinon.assert.calledOnce(fakeAuthorize);
const source = fakeAuthorize.getCall(0).args[0];
assert.strictEqual(source.userId, 'U-event-user');
});
});