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

feat: Implement multi token support for client tokens #1476

Merged
merged 1 commit into from
Apr 8, 2022

Conversation

sighphyre
Copy link
Member

This adds support for multi project tokens to be created. Backward compatibility is handled at 3 different layers here:

  • The API is made backwards compatible though a permissive data type that accepts either a project?: string or projects?: string[] property, validation is done through JOI here, which ensures that projects and project are not set together. In the case of neither, this defaults to the previous default of ALL_PROJECTS
  • The service layer method to handle adding tokens has been made tolerant to either of the above case and has been deprecated, a new method supporting only the new structure of using projects has been added
  • Existing compatibility for consumers of Unleash as a library should not be affected either, the ApiUser constructor is now tolerant to the the first input and will internally map to the new cleaned structure

This adds support for multi project tokens to be created. Backward compatibility is handled at 3 different layers here:

- The API is made backwards compatible though a permissive data type that accepts either a project?: string or projects?: string[] property, validation is done through JOI here, which ensures that projects and project are not set together. In the case of neither, this defaults to the previous default of ALL_PROJECTS
- The service layer method to handle adding tokens has been made tolerant to either of the above case and has been deprecated, a new method supporting only the new structure of using projects has been added
- Existing compatibility for consumers of Unleash as a library should not be affected either, the ApiUser constructor is now tolerant to the the first input and will internally map to the new cleaned structure
@vercel
Copy link

vercel bot commented Apr 6, 2022

This pull request is being automatically deployed with Vercel (learn more).
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: https://vercel.com/unleash-team/unleash-docs/6ESBYF9gkHZTqwJCAAA1LVDmJ2mQ
✅ Preview: https://unleash-docs-git-feat-multi-project-tokens-unleash-team.vercel.app

@github-actions
Copy link

github-actions bot commented Apr 6, 2022

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 91.19% 5094/5586
🟡 Branches 79.88% 794/994
🟢 Functions 86.23% 1202/1394
🟢 Lines 91.12% 4978/5463
Show files with reduced coverage 🔻
St.
File Statements Branches Functions Lines
🟢
... / api-token.ts
97.67% (-2.33% 🔻)
95.45% (-4.55% 🔻)
100%
97.3% (-2.7% 🔻)
🟢
... / api-token-service.ts
90%
57.14% (+1.59% 🔼)
94.74% 89.86%

Test suite run success

842 tests passing in 121 suites.

Report generated by 🧪jest coverage report action from e889d8e

secret text NOT NULL,
project text NOT NULL,
FOREIGN KEY (secret) REFERENCES api_tokens (secret) ON DELETE CASCADE,
FOREIGN KEY (project) REFERENCES projects(id) ON DELETE CASCADE
Copy link
Member

Choose a reason for hiding this comment

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

will this automatically remove the project from the token if the project is removed?
If yes: Awesome!

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah it will!

Copy link
Contributor

Choose a reason for hiding this comment

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

Cascade does rule :)

'seen_at',
'environment',
'token_project_link.project',
);
Copy link
Member

Choose a reason for hiding this comment

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

I am pretty sure this query can be written to actually return the list of projects as a string array directly from postgres using the "ARRAY_AGG" function. I am not sure if it is more efficient, but would at least avoid the need for the reducer processing the items.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh cool, I didn't actually know about ARRAY_AGG, that's super cool. I'll have to shuffle the query around but this shouldn't be a very hot path/large dataset so the readability wins out here for me anyway, thanks!

[newToken.secret, project],
);
});
await Promise.all(updateProjectTasks);
Copy link
Member

Choose a reason for hiding this comment

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

Yo do not need to create a bunch of insert request. You just need to to map the projects in it to a list of "objects" matching the "API_LINK_TABLE" shape and perform a simple knex insert on all the values.

Copy link
Contributor

Choose a reason for hiding this comment

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

What Ivar said, knex already has efficient batch insert, write the query, give a list of tuples with the same amount of args as your sql insert. See src/lib/db/clientMetricsStore-v2.ts:101 for instance.

Though this seems to have the advantage that it's transactional and as such will rollback the LINK_TABLE inserts if any of them fail.

Copy link
Member

Choose a reason for hiding this comment

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

that can be achieved anyway, and yes the transaction stuff is the important part here!

Copy link
Contributor

@chriswk chriswk left a comment

Choose a reason for hiding this comment

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

Great thorough PR, I think Ivar's comments warrants a check to see if it's more efficient, but this LGTM :)

const randomStr = crypto.randomBytes(28).toString('hex');
return `${project}:${environment}.${randomStr}`;
if (projects.length > 1) {
return `[]:${environment}.${randomStr}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

So multi project tokens now gets resolved from the db, fair enough :)

Copy link
Contributor

Choose a reason for hiding this comment

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

As opposed to not being supported at all, don't get me wrong :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it's a big tradeoff here. All the other token types can be determined just from their secret signature, this is the first time they're no longer stateful. Means we can't yeet them from Unleash instance to Unleash instance unfortunately but it keeps the token small

);
};

//This is a lossy down migration, tokens with multiple projects are discarded
Copy link
Contributor

Choose a reason for hiding this comment

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

That's fair, I'd be a bit surprised if I had multple project tokens and then ended up with separate tokens for each project if I reverted the thing that allowed me to have a token accessing multiple projects.

Copy link
Member Author

Choose a reason for hiding this comment

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

My thought too! I absolutely don't want them getting more permissive, and if you make them less permissive it's just weird. Clearly to destroy for everyone involved

@@ -198,7 +202,7 @@ const loadTokensFromString = (tokenString: String, tokenType: ApiTokenType) => {
type: tokenType,
username: 'admin',
};
validateApiToken(token);
validateApiToken(mapLegacyToken(token));
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, so, we won't have support for adding multiproject tokens from environment variables for this release.

Copy link
Member Author

Choose a reason for hiding this comment

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

That is correct! We need to come up with a separate format for the env vars, currently they're determined from the secret signature in this code path. The secret doesn't contain all the info needed for a multiproject. I think something like this would work:

We can read that string, calculate the projects and then strip them

[newToken.secret, project],
);
});
await Promise.all(updateProjectTasks);
Copy link
Contributor

Choose a reason for hiding this comment

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

What Ivar said, knex already has efficient batch insert, write the query, give a list of tuples with the same amount of args as your sql insert. See src/lib/db/clientMetricsStore-v2.ts:101 for instance.

Though this seems to have the advantage that it's transactional and as such will rollback the LINK_TABLE inserts if any of them fail.

secret text NOT NULL,
project text NOT NULL,
FOREIGN KEY (secret) REFERENCES api_tokens (secret) ON DELETE CASCADE,
FOREIGN KEY (project) REFERENCES projects(id) ON DELETE CASCADE
Copy link
Contributor

Choose a reason for hiding this comment

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

Cascade does rule :)

@sighphyre sighphyre merged commit c9b44b6 into main Apr 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants