Skip to content
Draft
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/file-store-delete-user-token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@slack/oauth": patch
---

fix(oauth): forget a deleted user's token in the latest `FileInstallationStore` installation
23 changes: 23 additions & 0 deletions packages/oauth/src/installation-stores/file-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
21 changes: 12 additions & 9 deletions packages/oauth/src/installation-stores/file-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`);
}
Expand All @@ -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 } }));
}
}
Loading