Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for FileInstallationStore #1279

Merged
merged 5 commits into from
Jul 14, 2021

Conversation

misscoded
Copy link
Contributor

@misscoded misscoded commented Jul 2, 2021

Summary

Fixes #1278:

Currently, the default (and only) InstallationStore made available to developers out of the box is the MemoryInstallationStore. This store isn't ideal for all development scenarios, as data is stored in memory and thus cleared out when the server is restarted.

We should introduce a FileInstallationStore, reasonably following the existing pattern(s) found on the Python and Java SDK side of things.


Exportability for Choice

Though MemoryInstallationStore remains the default if not otherwise specified, to allow developers the choice between using MemoryInstallationStore and FileInstallationStore, both are now exported for direct use in their projects (set to the installationStore constructor option):

const { InstallProvider, MemoryInstallationStore, FileInstallationStore } = require('@slack/oauth');

const installer = new InstallProvider({
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  authVersion: 'v2',
  stateSecret: 'my-state-secret',
  // installationStore: new MemoryInstallationStore(),
  installationStore: new FileInstallationStore({ baseDir: '.', historicalDataEnabled: false }),
});

Options include: baseDir, clientId, and historicalDataEnabled. Developers can go with the defaults, or override to suit their needs.

Installation Directory + File Pattern

For the most part, implementation has stayed true to what is found in Python, but some differences are detailed below:

  1. The enterprise_id and team_id are used to create the installation directory. If both present, the pattern is enterprise_id-team_id. If only one of the two are present, only that ID is used for the directory. This differs slightly from Python's use of none in the directory name if one is not passed.
  2. When storeInstallation is called, if historicalDataEnabled is enabled (which it is by default), 4 records will be created: an app-latest, user-[user_id]-latest, app-[currentUTC] and user-[user_id]-[currentUTC]. Every subsequent install will overwrite the data in latest and save two additional currentUTC records. If set to false by the developer, only *-latest records will be saved.

Screen Shot 2021-06-30 at 6 40 03 PM

  1. Python goes a step further by saving the same information into both installer-latest / installer-[currentUTC] and the user-[user_id]-* records. I omitted the former records because it wasn't clear to me how the duplication was value-adding, but am open to introducing it if folks feel it's important or if I've misunderstood its purpose.
  2. Python also makes use of a to_bot() method to shape the data prior to writing it to file, but for a similar reason to the above, I forwent that. Also equally happy to introduce it if it's the better (or necessary) approach.

Directory/File Restructuring

  1. New /stores directory. Since we now have two stores, I've pulled out the MemoryInstallationStore class into a stores directory, alongside the new FileInstallationStore. These two are then made available in a dedicated index.js so that the import is cleaner (both importable directly from /stores).

Screen Shot 2021-07-01 at 5 12 57 PM

Screen Shot 2021-07-01 at 5 14 01 PM

Requirements (place an x in each [ ])

@misscoded misscoded added semver:minor enhancement M-T: A feature request for new functionality pkg:oauth applies to `@slack/oauth-helper` draft labels Jul 2, 2021
@misscoded misscoded self-assigned this Jul 2, 2021
if (this.historicalDataEnabled) {
const currentUTC = Date.now();
writeToFile(`${installationDir}/app-${currentUTC}`, installationData);
writeToFile(`${installationDir}/user-${user.id}-${currentUTC}`, installationData);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this in the summary, but this is where it's applicable, so carrying it over:

The Python FileStoreInstallation goes a step further by saving the same information into both installer-latest / installer-[currentUTC] and the user-[user_id]-* records. I omitted the former records because it wasn't clear to me how the duplication was value-adding, but am open to introducing it if folks feel it's important or if I've misunderstood its purpose.

Copy link
Member

@seratch seratch Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically talking about this file based implementation, not having all the historical data is also fine as most developers won't use it for production.

The reason why we store all installations in Python / Java is that having the data can be helpful for troubleshooting / system operations in production. When you do investigation about an issue related to a user who installed the app multiple times, checking each installation's timestamp, the difference on its scopes, and the existence of enterprise_id may help.

If we think this file based one is a kind of reference implementation, having this consideration may be worth. Not a strong opinion here, though. Just having a comment about this context there is also fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally get it and am with you regarding the importance. 👍🏼

I think what I'm not understanding is the difference between these two records, other than the extra formatting done to the first, and why having both is necessary or beneficial.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider setting saving historical data default to false? Users could set it to true if they want to save historical data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion on this. Happy to go with whatever others deem the best default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what I'm not understanding is the difference between these two records, other than the extra formatting done to the first, and why having both is necessary or beneficial.

Even after revoking all the user installations, bot token must still exist. This is one of the major improvements in GBP (Granular Bot Permissions). This is the reason why Python / Java SDK have bots table separately.

If there is a better alternative way, we can go with a different approach in Node SDK first and implement the same in others, though. Having is_user_token_revoked flag in the installation table plus trying to fetch a bot token even when all installations are marked as deleted may work. Do we think this is a better approach? 🤔

Should we consider setting saving historical data default to false?

In Python / Java SDK, the historical data support is enabled by default. For consistency and encouraging a similar approach for production app design, I prefer turning it on but I am open to change this default for Node SDK (and possibly for others in the future major versions).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@misscoded @stevengill For enabling the historical data support by default or not, I'm happy to leave the decision to you all. I've already shared my thoughts on it in the above comment. If we want to change the defaults for Python / Java, we may want to hold off doing so until the next major releases (we won't do at least this year for both).

For having bots and user installations, as I mentioned above, having something similar (or user_token_revocation flag) should be necessary to keep a bot token even after the user(s) who installed the app are deactivated or all the user tokens are revoked. For these two store implementations, I think it's okay not to support the use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to have historical data enabled as the default, especially to maintain parity between the three languages. So we're good on that point! I'll keep it as is.

I'd like to revisit the other point you bring up when I circle back around to sort out the lack of support for user tokens as a whole, unless you think we shouldn't move forward with introducing this until that's sorted. I think I'm still fuzzy on the details.

}

public async fetchInstallation(query: InstallationQuery<boolean>, logger?: Logger): Promise<Installation> {
const { enterpriseId, teamId } = query;
Copy link
Contributor Author

@misscoded misscoded Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MemoryInstallationStore doesn't take into consideration user_id, so I omitted it here, but now that I'm reading over it in this context..

Given that user.id has been introduced in storeInstallation above (edit: and deleteInstallation below), my gut feeling is that it needs to be introduced here, as well, but confirmation of this from others would be useful.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about having TODO comments on user token scope installation support?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seratch so to support user tokens, it seems we would want to fetch /user-{user_id}-latest from the installationDirectory. How do you determine if you want to return this over app-latest? Do you look for the existence of user_id in the query and /user-{user_id}-latest on the file system and return it over app-latest? How about if you were explicitly looking for a user token existing, in that scenario if /user-{user_id}-latest didn't exist, you would want to throw an error instead of getting app-latest which might contain a user token belonging to a different user.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seratch this is very helpful! So the idea being do two fetchinstallation calls in authorize (or do this logic in fetchInstallatino itself). Roughly, in this JS implementation, the first fetchInstallation call would grab app-latest. We would want to strip out user_id from query so it doesn't try to grab a user-{user_id}-latest instead. The second call to fetchInstallation we would pass in the userId and it would look for user-{user_id}-latest. It would pull out the user portion of the installation Object and attach it to the original installationObject we get back from app-latest. It covers some of the edge cases I was worried about. Nice solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To limit the scope, I'm in favor of leaving a TODO to address the user token support at a later time if others are.

packages/oauth/src/index.ts Outdated Show resolved Hide resolved
Copy link
Member

@seratch seratch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't verified if it works fine but at first glance, the changes look good to me. Let a few comments.

packages/oauth/src/stores/file-store.ts Show resolved Hide resolved
if (this.historicalDataEnabled) {
const currentUTC = Date.now();
writeToFile(`${installationDir}/app-${currentUTC}`, installationData);
writeToFile(`${installationDir}/user-${user.id}-${currentUTC}`, installationData);
Copy link
Member

@seratch seratch Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically talking about this file based implementation, not having all the historical data is also fine as most developers won't use it for production.

The reason why we store all installations in Python / Java is that having the data can be helpful for troubleshooting / system operations in production. When you do investigation about an issue related to a user who installed the app multiple times, checking each installation's timestamp, the difference on its scopes, and the existence of enterprise_id may help.

If we think this file based one is a kind of reference implementation, having this consideration may be worth. Not a strong opinion here, though. Just having a comment about this context there is also fine.

packages/oauth/src/stores/file-store.ts Outdated Show resolved Hide resolved
}

public async fetchInstallation(query: InstallationQuery<boolean>, logger?: Logger): Promise<Installation> {
const { enterpriseId, teamId } = query;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about having TODO comments on user token scope installation support?

packages/oauth/src/stores/file-store.ts Show resolved Hide resolved
packages/oauth/src/index.ts Outdated Show resolved Hide resolved
@seratch
Copy link
Member

seratch commented Jul 2, 2021

This pull request seems to hav conflicts with the latest revision in main branch. Resolving it before you finalize this work would make things easier.

docs/_packages/oauth.md Outdated Show resolved Hide resolved
@misscoded misscoded removed the draft label Jul 9, 2021
@misscoded misscoded marked this pull request as ready for review July 9, 2021 00:48
@seratch seratch added this to the [email protected] milestone Jul 12, 2021
@seratch
Copy link
Member

seratch commented Jul 13, 2021

@misscoded
I don't have any further comments apart from #1279 (comment)

If we are comfortable to have this change in the next minor (v2.2 - the main update is the token rotation support), you can merge this pull request. Otherwise, you can move this PR and its issue to v2.3 milestone!

@misscoded
Copy link
Contributor Author

misscoded commented Jul 13, 2021

@seratch

If we are comfortable to have this change in the next minor (v2.2 - the main update is the token rotation support), you can merge this pull request. Otherwise, you can move this PR and its issue to v2.3 milestone!

I think we're good, just waiting on Steve to give the final go ahead on what's here.

@misscoded misscoded requested a review from seratch July 13, 2021 23:01
@misscoded misscoded merged commit 1a4497f into slackapi:main Jul 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement M-T: A feature request for new functionality pkg:oauth applies to `@slack/oauth-helper` semver:minor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for FileInstallationStore
3 participants