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

Implement permission policies in the API #22384

Draft
wants to merge 180 commits into
base: auditus
Choose a base branch
from
Draft

Conversation

rijkvanzanten
Copy link
Member

@rijkvanzanten rijkvanzanten commented May 3, 2024

Scope

What's changed:

  • Implements policy-system based permissions handling on the API
  • Replaces AuthorizationService with new set of functions in the api/src/permissions folder
  • Allows roles to be nested
  • Adds new roles flag to accountability object. This is an ordered array of all the parent roles of the current user
  • Cleans up get-ast-from-query by splitting it into multiple files
  • Permissions are now injected into the AST through cases and whenCase. This allows us to dynamically generate the case/when SQL to have dynamic field output per item.
  • Cleans up run-ast by splitting it up into smaller files

Potential Risks / Drawbacks

  • The risks are very high. This replaces the full permissions system, and thus needs a lot of testing

Review Notes / Questions

  • This PR now compiles, but doesn't run yet.
  • There was some weird logic happening in the users controller for TFA enable/disable that I'm not sure we need to keep. Needs a bit more testing

Todos

  • Add permissions processing for $CURRENT_USER etc flags
  • Add permissions merging when you're accessing from a share
  • Add caching to:
    • Fetching Policies
    • Fetching permissions
    • Fetching the roles tree
    • Fetching the field map
    • Fetching allowed fields
    • Fetching allowed collections
  • Enable validation for admin users for wrong paths
  • Figure out what we want to do with Presets
  • Use whenCases in run-ast
  • Use applyCases in Meta Service for permissions aware counts
  • Handle admin-checks in roles and users services1
  • Add unit testing for clear method in memory/cache
  • Make sure graphql gracefully handles optional fields when you have different fields for the same collection in multiple permission sets
  • Add changeset
  • Unbork /permissions endpoint
  • Invalidate cache on permission changes

Closes #21778, closes #21765, closes #22163, closes #21769, closes #21768, closes #21767, closes #21766

Footnotes

  1. Eg check to make sure there's still >=1 admin left after the mutation is done

} else {
paths.add(
// Ignore all operators and logical grouping in the field paths
path.filter((part) => part.startsWith('_') === false),
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably explicitly test for all known filter operators since otherwise valid field names that start with an underscore are ignored. (See my added, failing, test case).

* All nested paths used in the current query scope.
* This is generated by flattening the filters and adding in the used sort/aggregate fields.
*/
const paths: Set<FieldKey[]> = new Set();
Copy link
Contributor

Choose a reason for hiding this comment

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

Set<FieldKey[]> does not guarantee uniqueness of the path arrays, as ['author'] != ['author']. So we can switch this over to a list, but need to enforce uniqueness with uniqWith(paths, isEqual).

]);
});

test('Keeps policies that match the IP cidr block', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We're effectively only testing if the ipInNetworks function works, which is already covered in the test suite of the util function. So maybe mock it out and just check that is called correctly?

const key = namespace + '-' + getSimpleHash(JSON.stringify(args[0]));
const cached = await cache.get(key);

if (cached) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This right now only works for functions that return a non falsy value. If a cached function returns null, 0, '', false, etc. its value will not be returned from cache.

Should this be changed to

Suggested change
if (cached) {
if (cached !== undefined) {

@@ -74,23 +76,20 @@ export class AuthenticationService {

const user = await this.knex
Copy link
Contributor

Choose a reason for hiding this comment

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

Just as a note, since the user does not have the admin_access and app_access flags anymore, we need to look in to the auth providers and verify that they did not need that info, as the user object is passed down to their login method.

ast = await authorizationService.processAST(ast, opts?.permissionsAction);
}
ast = await processAst(
{ ast, action: 'read', accountability: this.accountability },
Copy link
Contributor

Choose a reason for hiding this comment

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

Did we drop support for QueryOptions.permissionsAction or is this accounted for somewhere else, that I'm missing?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we're planning to drop support for meta anyways (see #15665) I would suggest not investing any time in how to make this work and consider it a good point to drop it now rather than later? Maybe in a follow-up PR?

if (permissions.fields.includes('*') === false) {
const allowedFields = permissions.fields;
if (allowedFields.includes(field) === false) throw new ForbiddenError();
if (allowedFields.includes(field) === false) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is missing the check for '*'. Was that done on purpose? As far as I can see fetchAllowedFields does not expand '*' to all available fields of a collection, so explicitly checking for '*' seems necessary?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: 🏗 In progress
Development

Successfully merging this pull request may close these issues.

None yet

5 participants