Skip to content

Fix stale dynamic permission presets after editing current account#27899

Merged
ComfortablyCoding merged 11 commits into
directus:mainfrom
scarab-systems:fix-current-user-permission-context
Jul 24, 2026
Merged

Fix stale dynamic permission presets after editing current account#27899
ComfortablyCoding merged 11 commits into
directus:mainfrom
scarab-systems:fix-current-user-permission-context

Conversation

@scarab-systems

@scarab-systems scarab-systems commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What's Changed

  • Refresh permissions after the current user updates to keep dynamic presets up to date
  • Removed the redundant client-side preset parsing logic
  • Show saved PK (scalar) on a forbidden m2o relation instead of erroring

Tested Scenarios

  • Presets are always up to date with current user values
  • Forbidden M2O falls back to the saved key for scalar values in addition to object

Review Notes / Questions / Concerns

  • None

Checklist

Leave unchecked where not applicable

  • Tests added/updated
  • Documentation PR created in directus/docs
  • OpenAPI updated
  • SDK (@directus/sdk) updated to reflect the changes
  • Types (@directus/types) updated to reflect the changes
  • GraphQL schema updated to reflect the changes
  • System data (@directus/system-data) updated for changes to system collections/fields/relations
  • Database migration added for schema/system changes
  • Environment variables documented for new/changed config
  • App translations added for new user-facing strings
  • Security implications apply

Fixes #24029

Disclosure: I used AI-assisted coding tools while preparing this PR. I reviewed the changes myself, tested them, and take responsibility for the implementation and any follow-up revisions needed.

@scarab-systems
scarab-systems force-pushed the fix-current-user-permission-context branch from a4db98a to 5d3d334 Compare July 13, 2026 00:47
@kheiner

kheiner commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

If this project does not accept AI-assisted contributions, please let us know up front so we can avoid spending time on a contribution that the project is unwilling to consider.

AI Assisted contributions are OK.

You'll also want to read the Contribution Guidelines.

I don't understand the relevance of what you've outlined in the 'tested scenarios' section, you might want to review the Common Commands to simplify your efforts there.

@scarab-systems

Copy link
Copy Markdown
Contributor Author

Appreciate the pointers. I simplified the Tested Scenarios section to align with the common command wording, and removed the now-unneeded AI policy caveat from the disclosure.

@kheiner

kheiner commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Your changes can't be merged until you add yourself to the contributors list; I don't think the Directus Team will review your code until you do that.

@scarab-systems

scarab-systems commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Added scarab-systems to contributors.yml in the latest commit.

@ComfortablyCoding ComfortablyCoding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR! Unfortunately I am unable to reproduce the original issue on the latest version, so I can't confirm whether this fixes it or not. Could you share the steps to reproduce the issue?

Reproduction Attempt

Attempt was done with sqlite, db should not play a role here. No cache enabled.

Screen.Recording.2026-07-17.at.11.35.58.AM.mov
Schema

snapshot.json

@scarab-systems

Copy link
Copy Markdown
Contributor Author

Thanks, that helped clarify the gap. My current patch covers the stale $CURRENT_USER.* permission-preset side of the report after saving the signed-in user, but the original issue also includes the relationship field display behavior.

I’m going to broaden the patch so the PR covers the full reported user-save path rather than only the permissions rehydration side. I’ll update the tests/repro notes accordingly so it’s clear which behavior is covered.

@scarab-systems

Copy link
Copy Markdown
Contributor Author

Thanks again for the reproduction note. I pushed an update in 316e0efeb6 that broadens the patch as noted above.

The PR now covers both app-side paths from the original report:

  • after saving the signed-in user, the current user and permissions stores are refreshed so $CURRENT_USER.* permission presets do not keep stale user values;
  • for M2O relationship fields, if the saved related item cannot be read and the API returns 403, the field now keeps displaying the saved primary key instead of blanking the value.

I also added focused coverage for the broadened relation-display path in use-relation-single.test.ts, while keeping the existing signed-in user save test.

Checks run with Node 22:

  • pnpm exec prettier --check .changeset/fresh-users-remember.md app/src/composables/use-relation-single.ts app/src/composables/use-relation-single.test.ts app/src/modules/users/routes/item.vue app/src/modules/users/routes/item.test.ts
  • pnpm exec eslint app/src/composables/use-relation-single.ts app/src/composables/use-relation-single.test.ts app/src/modules/users/routes/item.vue app/src/modules/users/routes/item.test.ts
  • pnpm --dir app exec vitest run src/composables/use-relation-single.test.ts src/modules/users/routes/item.test.ts

Those focused checks passed locally.

@ComfortablyCoding

ComfortablyCoding commented Jul 17, 2026

Copy link
Copy Markdown
Member

Appreciate the updates, but neither of those addresses my review. I am unable to reproduce the original issue and therefor cannot verify whether the proposed fix resolves it.

@scarab-systems

Copy link
Copy Markdown
Contributor Author

Thanks, you’re right. My previous updates did not give the actual reproduction path you asked for.

I can reproduce the original report when I use the version/environment from the issue rather than a current unlicensed setup:

  • directus/directus:11.2.1 Docker image
  • Postgres
  • no cache
  • a tenant collection with UUID primary keys
  • directus_users.tenant_id and directus_users.builder_tenant_id as nullable M2O fields to tenant
  • an author collection with required tenant_id M2O to tenant
  • a non-admin app user with:
    • read/update self permissions on directus_users, including tenant_id and builder_tenant_id
    • read permission on tenant
    • create permission on author with this preset:
{
  "tenant_id": "$CURRENT_USER.builder_tenant_id"
}

Steps that reproduce it:

  1. Login as that app user while builder_tenant_id is still null.
  2. Open the signed-in user’s own user page.
  3. Set only Builder Tenant ID to a tenant and save.
  4. Without refreshing, logging out, or logging back in, go to Author and create an item with only Title set.

Observed result on 11.2.1:

  • the user save succeeds and the database has the new builder_tenant_id;
  • the app does not refetch /permissions/me after that user save;
  • the Author create form still has the old parsed preset value, so tenant_id is blank/null;
  • saving fails client-side with Tenant ID: Value can't be null, and no POST /items/author is sent.

I also checked the backend side separately: after updating /users/me, a same-token API-only POST /items/author succeeds and applies the new tenant. So the failure is specifically the app-side parsed permissions/default-value path, not server-side $CURRENT_USER resolution.

One note about the latest/sqlite reproduction attempt: on directus/directus:12.1.1 Docker with sqlite and no license, I could not create the exact original permission setup because POST /permissions with that custom preset is rejected with 403 RESOURCE_RESTRICTED for custom_permission_rules_enabled. So an unlicensed latest/sqlite setup does not reach the same condition as the original 11.2.1 report unless custom permission rules are enabled or the project already has that preset.

The permissions-store refresh in this PR is the change tied to the reproduced create failure above: after the current user is hydrated from the save, the permissions store is hydrated again so $CURRENT_USER.builder_tenant_id presets are parsed from the updated user value before the next create form is opened.

@ComfortablyCoding

Copy link
Copy Markdown
Member

Thanks for the reproduction steps, I can reproduce the issue now. 🙂

That said, I don't think refreshing permissions is the correct solution here. It addresses the symptom rather than the root cause, since the permissions themselves are not actually stale in this case.

The stale value is the preset. parsePreset() currently runs once at login, which means any values it depends on that change at runtime can become incorrect.

I believe a better approach would be to remove parsePreset() from the permissions store and compute the preset value when it is needed instead of resolving it once during login. This keeps it in sync with any runtime changes without requiring a permissions refresh.

Thoughts?

@scarab-systems scarab-systems changed the title Refresh permissions after current user save Parse current-user permission presets when read Jul 20, 2026
@scarab-systems

scarab-systems commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that makes sense.

I updated the PR in 12c463b8f4 to follow that direction instead of refreshing permissions after the user save.

The permissions store now keeps the raw preset payload from /permissions/me, and getPermission() parses dynamic preset values when the permission is read. That means $CURRENT_USER.* values resolve against the current user state at the point of use rather than the login-time snapshot.

I also removed the route-level permissions refresh and its regression test, and added store-level regression coverage that verifies the preset updates after the current user changes without another /permissions/me request. The relation-display fallback from the broader original report remains in place.

Happy to adjust further if you would prefer this shaped differently.

@ComfortablyCoding

Copy link
Copy Markdown
Member

Thanks for the updates! This is definitely an improvement over the re-fetch approach, but I don't think this is the right place for it.

getPermissions now has a side effect, whereas it should ideally remain a pure function. It also means we parse the presets every time getPermissions is called, even when the resolved presets are not needed (e.g. hasPermission).

I believe the better approach would be to leave getPermissions as-is and resolve the presets where they are consumed. At the moment, the only usage I’m aware of is in getFields so that seems like the more appropriate place to handle it.

This would keep the preset values in sync with runtime changes without introducing side effects or unnecessary work into getPermissions.

Thoughts?

@scarab-systems

Copy link
Copy Markdown
Contributor Author

Thanks, that makes sense. I updated this in 6ca8aef6bbdb to move preset parsing out of the permissions store and into getFields, where the preset values are actually consumed as field defaults.

getPermission() is back to returning the raw permission payload, so callers like hasPermission() do not parse presets just to check access.

The updated tests now cover both sides of that boundary:

  • the permissions store keeps $CURRENT_USER.* presets raw after hydrate/read
  • getFields resolves dynamic preset defaults against the current user at consumption time, including after the current user state changes

The existing relation-display fallback from the broader report remains in place.

Local focused checks passed:

  • pnpm --filter @directus/app test app/src/stores/permissions.test.ts app/src/composables/use-permissions/item/lib/get-fields.test.ts app/src/composables/use-relation-single.test.ts
  • Prettier and ESLint on the touched files
  • git diff --check

The current PR check rollup is green as well. Happy to adjust further if you would prefer the helper placement or naming shaped differently.

The /permissions/me endpoint already resolves $CURRENT_USER and $CURRENT_ROLE
preset variables server-side, so the client-side parsePreset and related operations are redundant.
@ComfortablyCoding

ComfortablyCoding commented Jul 24, 2026

Copy link
Copy Markdown
Member

After further investigation, the /permissions/me endpoint was already returning resolved presets. The frontend code gave the false impression that the backend was returning raw values, which led down the wrong path.

Apologies for the confusion, the original solution appears to be the right approach given the current state. I’ve reverted the changes back to that implementation and removed the outdated code 🫡

@ComfortablyCoding ComfortablyCoding changed the title Parse current-user permission presets when read Fix stale dynamic permission presets after editing current account Jul 24, 2026

@ComfortablyCoding ComfortablyCoding left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thanks and congratulations on your first contribution to directus! 🥳

@ComfortablyCoding
ComfortablyCoding merged commit a2ad746 into directus:main Jul 24, 2026
74 checks passed
@github-actions github-actions Bot added this to the Next Release milestone Jul 24, 2026
@scarab-systems

Copy link
Copy Markdown
Contributor Author

Woohoo! Thank you! Glad we could help. 🙏

@scarab-systems
scarab-systems deleted the fix-current-user-permission-context branch July 24, 2026 17:52
scarab-systems added a commit to scarab-systems/scarab-field-lab that referenced this pull request Jul 24, 2026
Record directus/directus#27899 merge and issue closure. Note the public maintainer correction confirming the original refresh-permissions approach was the right one for the current implementation. Update validation, review status, public links, and changed-file coverage for the merged PR.
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.

Relationship Field on User Not Appearing or Saving to CURRENT_USER object

3 participants