diff --git a/.changeset/file-store-delete-user-token.md b/.changeset/file-store-delete-user-token.md new file mode 100644 index 000000000..a542beff3 --- /dev/null +++ b/.changeset/file-store-delete-user-token.md @@ -0,0 +1,5 @@ +--- +"@slack/oauth": patch +--- + +fix(oauth): forget a deleted user's token in the latest `FileInstallationStore` installation diff --git a/packages/oauth/src/installation-stores/file-store.test.ts b/packages/oauth/src/installation-stores/file-store.test.ts index 3e3d584ed..eecaefa31 100644 --- a/packages/oauth/src/installation-stores/file-store.test.ts +++ b/packages/oauth/src/installation-stores/file-store.test.ts @@ -213,6 +213,29 @@ describe('FileInstallationStore', async () => { assert.strictEqual(fsUnlinkSync.callCount, 1); }); + it('should forget the user token kept with the latest installation when that user is deleted', async () => { + const installationStore = new FileInstallationStore({ baseDir: os.tmpdir() }); + const { enterprise, team, user } = storedInstallation; + const fakeInstallDir = path.join(os.tmpdir(), `${enterprise.id}-${team.id}`); + const query = { enterpriseId: enterprise.id, teamId: team.id, userId: user.id }; + + await installationStore.deleteInstallation(query); + + const forgotten = JSON.stringify({ ...storedInstallation, user: { id: user.id } }); + assert.strictEqual(fsWriteFileSync.calledWith(path.join(fakeInstallDir, 'app-latest'), forgotten), true); + assert.strictEqual(fsUnlinkSync.calledWith(sinon.match('app-latest')), false); + }); + + it('should keep the latest installation untouched when another user is deleted', async () => { + const installationStore = new FileInstallationStore({ baseDir: os.tmpdir() }); + const { enterprise, team } = storedInstallation; + const query = { enterpriseId: enterprise.id, teamId: team.id, userId: 'otherUserId' }; + + await installationStore.deleteInstallation(query); + + assert.strictEqual(fsWriteFileSync.called, false); + }); + it("should run authorize with triage-bot's MongoDB data", async () => { // Refer to https://github.com/slackapi/bolt-js/issues/1265 to learn the context const storedInstallation = { diff --git a/packages/oauth/src/installation-stores/file-store.ts b/packages/oauth/src/installation-stores/file-store.ts index 3c2b21400..bba5c7799 100644 --- a/packages/oauth/src/installation-stores/file-store.ts +++ b/packages/oauth/src/installation-stores/file-store.ts @@ -104,20 +104,16 @@ export default class FileInstallationStore implements InstallationStore { logger.info(`Deleting installations in ${installationDir} with the following query: ${JSON.stringify(query)}`); } - let filesToDelete: string[] = []; - - if (userId === undefined) { - const allFiles = fs.readdirSync(installationDir); - filesToDelete = filesToDelete.concat(allFiles); - } else { - const userFiles = fs.readdirSync(installationDir).filter((file) => file.includes(`user-${userId}-`)); - filesToDelete = filesToDelete.concat(userFiles); - } + const files = fs.readdirSync(installationDir); + const filesToDelete = userId === undefined ? files : files.filter((file) => file.includes(`user-${userId}-`)); try { for (const filePath of filesToDelete) { deleteFile(path.join(installationDir, filePath)); } + if (userId !== undefined && files.includes('app-latest')) { + forgetUserCredentials(path.join(installationDir, 'app-latest'), userId); + } } catch (err) { throw new Error(`Failed to delete installation from FileInstallationStore (error: ${err})`); } @@ -139,3 +135,10 @@ function writeToFile(filePath: string, data: string): void { function deleteFile(filePath: string): void { fs.unlinkSync(path.resolve(filePath)); } + +function forgetUserCredentials(filePath: string, userId: string): void { + const installation: Installation = JSON.parse(fs.readFileSync(filePath).toString()); + if (installation.user.id === userId) { + writeToFile(filePath, JSON.stringify({ ...installation, user: { id: userId } })); + } +}