Skip to content

feat: New granular save and edit permissions#7896

Draft
lukaskleinschmidt wants to merge 12 commits intogetkirby:develop-minorfrom
lukaskleinschmidt:feat/save-and-edit-permissions
Draft

feat: New granular save and edit permissions#7896
lukaskleinschmidt wants to merge 12 commits intogetkirby:develop-minorfrom
lukaskleinschmidt:feat/save-and-edit-permissions

Conversation

@lukaskleinschmidt
Copy link
Contributor

Description

Adds granular more save and edit permissions for versioned content.
There are still a few concerns regarding the implementation and possible side effects.

I'm not completely happy with the current Vue implementation as it requires passing down the permissions to the FormControls component.
But I haven't found a better way to do it yet.

Second the way form fields currently get disabled in the Kirby\Form\Fields class is tied to a specific action name.
Which kinda works for the use case here, but it clashes for example with the permissions for langugages which only have a update permission and don't use versioned content. Which is not an issue as far as I can tell, but still not ideal.

}
}

public function for(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I noticed that the bool return type is expected but not really enforced and could lead to errors.
Maybe it should be more strict and always return false if a check does not yield the expected return type?

Example:
$permissions->for('pages') would throw an exception.
$extendedActions might deviate from the convention.

Copy link
Member

Choose a reason for hiding this comment

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

You mean that an $extendedAction could be defined with a non-boolean value, which will make return $this->actions[$category][$action]; try to return it, causing a TypeError?

To be honest I prefer a TypeError over silent conversion to false (makes it easier to spot issues in the code). However I think PHP sometimes tries to be smart about the conversion and doesn't always trigger a TypeError. So an alternative could be to throw a LogicException for any values that aren't booleans.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Throwing a exception would make the intended behaviour consistent. So a LogicException for non false values works for me.

Copy link
Contributor Author

@lukaskleinschmidt lukaskleinschmidt Jan 23, 2026

Choose a reason for hiding this comment

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

Now that I'm looking at it again the main headache came from the imposed bool $default = false.
I would assume it to return the default if no matching or expected permission was found.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, but I think there's a difference between "no matching permission" (which is the use case for the default) and "invalid permission" (which points to a bug).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fa1acb0 I could also add the exception in the translations but not sure when/why to pick one over the other.

throw new LogicException(
	key: 'permission.type',
	data: ['key' => $key, 'type' => gettype($permission)]
);

Copy link
Member

Choose a reason for hiding this comment

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

In an ideal world, all exceptions would be translated. However we sometimes omit that for exceptions that are unlikely to be thrown to the user and/or unlikely to occur at all. Because every translation key needs to be translated by all our community translators, but it doesn't really seem worth it for all exceptions.

@lukaskleinschmidt lukaskleinschmidt marked this pull request as draft January 22, 2026 09:18
@lukaskleinschmidt lukaskleinschmidt changed the title [WIP] feat: New granular save and edit permissions feat: New granular save and edit permissions Jan 22, 2026
Copy link
Member

@lukasbestle lukasbestle left a comment

Choose a reason for hiding this comment

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

General comment: The alias solution for the old update permission (with callback and array support) looks very useful and clean. 👍 At first glance I think it perfectly covers the migration matrix from the concept, we should test the different variants at the end of review before the PR gets merged though.

Three more uses of the update permission we IMO need to adapt:

  • config/areas/languages/views.php:35
  • Panel\File::isFocusable()
  • Panel\Ui\Buttons\LanguageSettingsButton

];

// normalize core actions
$this->actions = $this->normalize(
Copy link
Member

Choose a reason for hiding this comment

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

Question: As the update permissions will be deprecated and aliased, do we need to set them in the actions array? I honestly don't know how the aliases in individual model blueprint classes interact with the Permissions class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could you elaborate a bit more. Don't quite get the implication right now.

Copy link
Member

Choose a reason for hiding this comment

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

I don't either TBH. We will need to look into this in more detail.


if (is_array($settings) === true) {
return $this->setCategories($settings);
protected function normalize(
Copy link
Member

Choose a reason for hiding this comment

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

Thought: TBH I think this combined normalize() method is quite hard to read. We are actually trying to get away from long methods with a lot of logic. The old separate methods were a lot more readable and easier to follow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Problem was that I need the context of what was passed in initially to not overwrite already set values with an alias (L200). The seperate methods would make it way harder to keep that context.

I also dont like the big nested foreach + if list but it was actually the most sane way I could think of with all the back referencing and only setting not yet set values during the normalization.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will have another stab at this and see if I can make it more maintainable

Copy link
Member

Choose a reason for hiding this comment

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

I think it also depends on the other question above. Maybe @bastianallgeier can comment on that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just added a refactored version for the normalization 0c0b390 that makes it a bit more easy to read

Copy link
Member

Choose a reason for hiding this comment

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

I like the new refactored version for its readability. However I'm still not 100% sure the aliasing should happen in this class. We will discuss this in our team meeting today.

@lukaskleinschmidt
Copy link
Contributor Author

I think the languages do not use content versions/drafts at all. So not sure how we would tie that in or if it makes sense to add the distinction.

@lukasbestle
Copy link
Member

I think the languages do not use content versions/drafts at all. So not sure how we would tie that in or if it makes sense to add the distinction.

Yes, language settings and language variables are saved directly to the PHP files, there are currently no versions for those. We could add version support later and we also have plans for separate permissions for language variables. But that doesn't need to happen in this PR. I think it's fine to keep languages.update in the current setup.


public function for(
string|null $category = null,
string $category,
Copy link
Member

Choose a reason for hiding this comment

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

Question: Is there a specific reason why we need to remove the option to pass $category = null? This would be a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. Just figured there would be no use for this method without at least specificing a category. But could be rolled back easily

Copy link
Member

@lukasbestle lukasbestle Feb 28, 2026

Choose a reason for hiding this comment

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

You are right, that doesn't make any sense. The old implementation requires $category to be string as all called methods and the array access cannot deal with null.

Suggestion: For this PR, keep the nullable argument but check for it and call:

Helpers::deprecated('Passing `$category = null` to `Permissions::for()` is not supported', 'permissions-for-category-null')

We could then switch it over in 6.0, which is technically a breaking change then but probably not relevant in practice. But with the deprecation warning, we could find any remaining uses of this early.

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.

3 participants