Skip to content

feat: New Guards classes - #8364

Draft
bastianallgeier wants to merge 16 commits into
v6/developfrom
v6/feat/guards
Draft

feat: New Guards classes#8364
bastianallgeier wants to merge 16 commits into
v6/developfrom
v6/feat/guards

Conversation

@bastianallgeier

@bastianallgeier bastianallgeier commented Aug 7, 2026

Copy link
Copy Markdown
Member

Review

  • Design & concept
  • Rough pass
  • Deep read
  • Security check

Timing: No pressure.

Description & Docs

See https://github.com/getkirby/kirby/tree/v6/feat/guards/src/Guards#readme

Changelog

🎉 Features

  • New guards system: a single place that answers whether an action on a page, file, user, site or language is possible at all, allowed for the current user and valid for the given input.
  • Every model has a new guards() method to ask those questions directly.
  • Avatars can now be controlled with their own permissions for creating, replacing and deleting, instead of only the general update permission.
  • Changing a user's secrets (two-factor codes, passkeys) can now be controlled with its own permission.

✨ Enhancements

  • Blocked actions now explain themselves. "The home page cannot be deleted" instead of a generic "you are not allowed" message.
  • Errors now tell apart what is impossible for everyone from what the current user is simply not allowed to do.
  • Several error messages that were English-only can now be translated.
  • Permissions are resolved in a clear order: the model blueprint first, the role second, the default last.
  • The Panel can ask whether an action is allowed without knowing its input yet, which makes buttons and fields easier to enable or disable correctly.

🐛 Bug fixes

  • Changing a user's role or secrets no longer fails with an unexpected error when nobody is logged in.
  • Actions now run their checks against the model that a before hook returned, not the original one.

♻️ Refactored

  • All checks for page, file, user, site and language actions now live in one place instead of being spread over three.
  • The old rules and permission classes keep working and pass their work on to the guards.
  • Languages now share a common base class with pages, files, users and the site.

☠️ Deprecated

  • PageRules, FileRules, UserRules, SiteRules and LanguageRules. Use $model->guards() instead.
  • $model->permissions() and the permission classes behind it. Use $model->guards() instead.

🚨 Breaking changes

  • Actions that are impossible for everyone now throw a new ability error instead of a permission error. This affects deleting or moving the home and error page, sorting, changing the template of the error page, demoting the last admin and deleting the last language.
  • Several error messages and message keys changed.
  • The translation key error.page.changeStatus.toDraft.invalid was replaced by separate keys for the home page and the error page.
  • Changing a file or page to an invalid template now throws an input error instead of a logic error.
  • Deleting an avatar that does not exist now throws a user error instead of a "file not found" error.

Comment thread src/Cms/File.php
Comment on lines +387 to +402
public function isSameAs(BaseFile $file): bool
{
if ($this->exists() === false) {
return false;
}

// the model is based on the props of the new file,
// to compare templates, we need to get the props of
// the already existing file from meta content file
$existing = $this->parent()->file($this->filename());

return
$this->sha1() === $file->sha1() &&
$this->template() === $existing->template();
}

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.

I would think there will be questions in the future what's the difference between this and ::is() - and when to use which.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Would you fold it into ::is or change the name?

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.

I am not sure, I think :is() works also for virtual files etc. while ::sha1() would want to read an actual file to do this check. Wondering if this doesn't belong rather into FileValidators.

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.

Otherwise maybe ::isIdentical(). ::is() = actually it is the same object (well kinda, actually just the same id), ::isIdentical() - could be different objects, but they are identical.

Comment thread src/Cms/Language.php Outdated
Comment thread src/Cms/Media.php
{
// never publish risky files (e.g. HTML, PHP or Apache config files)
FileRules::validFile($file, false);
$file->guards()->validators()->validateFile(false);

@distantnative distantnative Aug 7, 2026

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.

Suggestion: Use named parameter to know what false refers to.

Comment thread src/Cms/Permissions.php
'delete' => true,
'deleteAvatar' => true,
'list' => true,
'replaceAvatar' => true,

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.

Discussion: Seeing these, I am wondering if long term it really makes sense to have these three avatar permissions. Are there use cases to allow creating and deleting but not replacing? Could this be tied to the user.update/users.update permissions - is the avatar here special vs. any user content field? Doesn't need to be solved by this PR but maybe good to think about and maybe add a TODO.

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.

Or maybe has to be solved here - just realized this PR introduces them :D Why do you think we need these?

Comment thread src/Cms/User.php
}

// `UserRules` enforces a minimum length of 8 characters,
// `UserValidators` enforces a minimum length of 8 characters,

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.

Suggestion: Shouldn't a lot of this code migrate to UserValidators?

Comment thread src/Guards/ModelPermissions.php Outdated
* @param bool $default Used if no rule is defined for the action
* @throws PermissionException
*/
protected function trySetting(string $action, bool $default = false): void

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.

I think here ensureSetting would Athen also become a stronger name.

Comment thread src/Guards/README.md
Comment thread src/Guards/README.md
Comment on lines +234 to +236
## The dry run

The dry run is everything that can be checked without the arguments of an action. It runs the ability check first and the permission check second, and throws for the first one that fails:

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.

Nitpick: I think "dry run" is a wrong analogy - these methods are not actually trying to execute the action, whether it fails, what the results would be... they just check the preconditions.

Comment thread src/Guards/README.md

The `error.` prefix is added by the exception classes, so no key in the guards ever contains it. Every key needs a matching entry in the translations, otherwise the generic fallback message of the exception is shown.

## Adding a new action

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.

Maybe good to mention that these are all optional? Otherwise it could seem quite laborious to add a new action if I have to implement all of these.

Comment thread src/Guards/README.md
}
```

That difference is worth remembering: replacing one of the three check classes changes one step in every action that uses it, adding a method to the guards class changes a single action completely.

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.

Question: Those all show custom X for specific models. What's our take on how to implement custom permission that isn't tied to a specific model?

Comment thread src/Cms/Permissions.php
'changeName' => true,
'changePassword' => true,
'changeRole' => true,
'changeSecret' => true,

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.

Question: Wondering if changing secrets should be its own permission. Or if this should be folded with changePassword somehow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants