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 214 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
    • Introduce $CURRENT_POLICIES and $CURRENT_ROLES for permissions
    • Decide if $CURRENT_POLICIES and $CURRENT_ROLES should be available in presets as well
  • 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
    • On directus_access changes
    • On directus_roles changes
    • On directus_permissions changes
    • On directus_policies changes
  • Bring back check-ip middlewares?
  • Fix down migration
  • Fix telemetry user counting
  • Fix user limit checking
  • Reduce accountability used in the withCache to the known keys
  • Account for user IP in the global request cache
  • Before merging this one in main, either retarget Add roles and permissions to the app #22654 to main, or merge it in here

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

@directus directus deleted a comment from 2chiefk Jun 3, 2024
router.search('/', validateBatch('read'), readHandler, respond);

router.get(
'/me/flags',
Copy link
Contributor

Choose a reason for hiding this comment

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

Anyone have a better name for this? Since in the future it might not only be flags. Should it follow the naming of the libs and maybe be called /me/global? The idea behind this endpoint is to accumulate any properties that are derived when taking all policies of a user into account, like global access and currently the enfore_tfa flag.

Copy link
Contributor

Choose a reason for hiding this comment

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

Mhh agreed /me/globals sounds better than flags to me too.

The idea behind this endpoint is to accumulate any properties

Mhh so maybe /me/properties ? 😄

If we dont expand on it /me/access sounds pretty fitting for right now, but yeah that might become less good in the future.

Copy link
Member

Choose a reason for hiding this comment

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

Would just /me work? 👀

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I personally dislike just /me because the endpoint does not return any policies, but some aggregation of all policies of the user, and we might end up with an actual /me endpoint that returns all policies at some point? wdyt?

Copy link
Member

Choose a reason for hiding this comment

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

Can you give an quick example of how the current response looks like?

Copy link
Contributor

Choose a reason for hiding this comment

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

It is effectively a condensed version of whatever flags we have on policies right now, that apply to a user.

{ "app_access": true, "admin_access": false, "enforce_tfa": false }

Copy link
Member

Choose a reason for hiding this comment

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

Okay, I think global(s) sounds more appropriate, that's ultimately what we're retrieving here: global flags/properties/whatever.

but some aggregation of all policies of the user, and we might end up with an actual /me endpoint that returns all policies at some point? wdyt?

You mean at a later point or is that something you'd like to look into now?

Copy link
Contributor

Choose a reason for hiding this comment

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

At a later point. Currently I don't see the need of retrieving all the policies of a user efficiently, as the app does not need to know them to work. But maybe that need arises at some point in the future, so I'd like to keep that option alive.

Comment on lines +12 to +14
throw new ForbiddenError({
reason: `You don't have permission to access collection "${collection}" or it does not exist. Queried in ${pathSuffix}.`,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Even though this, and the other errors are really helpful, they do expose that the collection + field exists, as it differs from the standard ForbiddenError in case of a non-existent collection.

Comment on lines -50 to -53
vi.mock('./middleware/check-ip', () => ({
checkIP: Router(),
}));

Copy link
Member

Choose a reason for hiding this comment

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

The mocking for ./middleware/get-permissions below should be removed too

id: roleID,
...defaultAdminRole,
});
await db('directus_roles').insert({ ...defaultAdminRole, id: roleID });
Copy link
Member

Choose a reason for hiding this comment

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

A new policy should be created

Suggested change
await db('directus_roles').insert({ ...defaultAdminRole, id: roleID });
await db('directus_roles').insert({ ...defaultAdminRole, id: roleID });
await db('directus_policies').insert({ ...defaultAdminPolicy, id: policyID });
await db('directus_access').insert({ policy: policyID, role: roleID });

router.search('/', validateBatch('read'), readHandler, respond);

router.get(
'/me/flags',
Copy link
Member

Choose a reason for hiding this comment

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

Would just /me work? 👀

Comment on lines 86 to 88
const query = { ...req.sanitizedQuery, limit: -1 };

const roles = await service.readMany(req.accountability.roles, query);
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 add the same comment as per other usages of this limit: -1 query?

Suggested change
const query = { ...req.sanitizedQuery, limit: -1 };
const roles = await service.readMany(req.accountability.roles, query);
// TODO fix this at the service level
const temporaryQuery = { ...req.sanitizedQuery, limit: -1 };
const roles = await service.readMany(req.accountability.roles, temporaryQuery);

if (this.accountability?.admin) {
return mapValues(this.schema.collections, () =>
Object.fromEntries(
['create', 'read', 'update', 'delete', 'share'].map((action) => [
Copy link
Member

Choose a reason for hiding this comment

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

Could we extract this array as a constant? This array will be used in the App as well.

Comment on lines +86 to +89
const query = { ...req.sanitizedQuery, limit: -1 };

try {
const roles = await service.readMany(req.accountability.roles, query);
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 include this comment about it being temporary to be consistent with other usages?

Suggested change
const query = { ...req.sanitizedQuery, limit: -1 };
try {
const roles = await service.readMany(req.accountability.roles, query);
// TODO fix this at the service level
const temporaryQuery = { ...req.sanitizedQuery, limit: -1 };
try {
const roles = await service.readMany(req.accountability.roles, temporaryQuery);

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

7 participants