How to get custom permissions from a page blueprint (when it is not created yet)

So I have this setup:

Editors can’t create pages by default. They can create pages of the type ‘note’, tho.

blueprints/users/editor.yml

  pages:
    create: false

blueprints/pages/note.yml

options:
  create:
    editor: true

In a frontend list template I want to show a “Create note”-Button for users with the right permission only. I could get the first child of the current page and request the permissions there. Something like $page->children()->first()->permissions()->can('create').

But what if there are no children yet?

You’d better check for the user role, not permissions.

Thanks for the super quick response. But I don’t see how hardcoding role names in my code would be better than using permissions. I mean that would defeat the whole purpose of permissions, wouldn’t it?

In order to use something like this in my template:

<?php if (NotePage::can($kirby->user(), 'create')): ?>
    <button>Create Note</button>
<?php endif ?>

I hacked something together which checks the page permissions first (if present), otherwise uses the users permissions:

/models/note.php

class NotePage extends Page
{
    static function can($user, $action)
    {
        if (!$user) {
            return false;
        }

        $pageBlueprint = Blueprint::load('pages/' . basename(__FILE__, '.php'));

        $currentPagePermission = $pageBlueprint['options'][$action] ?? null;

        // Current page permission for matching user role
        if (!empty($currentPagePermission[$user->role()->name()])) {
            return $currentPagePermission[$user->role()->name()];
        }

        // Current page permission for all roles
        if (!empty($currentPagePermission['*'])) {
            return $currentPagePermission['*'];
        }

        // User's page permission for all pages
        return $user->role()->permissions()->for('pages', $action);
    }
}

But this feels like I’m reinventing a wheel here. Is there nothing in that regard hidden deep in Kirbys toolbelt?

When a page is created in Kirby, Kirby creates a temporary page object first and can then check permissions on the page object. But in your case, you only want to show a button, in which case I think it probably doesn’t make sense to create a page object first, and it is easier to just check if you have a Kirby user and if this user has a particular role, instead of reading the blueprint, because of the overhead. Of course, if you later change your permissions, you would have to change the template as well, so abstracting this away is more flexible.

1 Like

I’ve moved the method to the user model and made a little plugin out of it.

Now I can do this in my templates:

if ($kirby->user()?->can('create', 'note')): 
    // show create button
endif

This is the cleanest solution I can come up with with my limited knowledge of Kirby :slight_smile:

If someone else needs this functionality, too, you can put this in your plugins folder:

/site/plugins/user-methods/index.php

<?php

/**
 * userPagePermissions
 *
 * Plugin to get the user's permissions for different page types
 * even if you don't have a page object (because the page is not created yet).
 *
 * PHP Version 8.x
 *
 * This will look in the page's blueprint for custom permissions first.
 * Only if it finds none, will it return the global page permissions for the user's role.
 *
 * Different from Kirby's implementation it will not return false if there is a custom
 * page permission for a different role but not for the role of your requested user.
 * Instead it will return the role's global permission.
 *
 * Usage: if ($kirby->user()?->can('create', 'note')) { // allowed  }
 */

use Kirby\CMS\Blueprint;

Kirby::plugin('elements/userPagePermissions', [
	'userMethods' => [
		/**
		 * Method to get the users permissions for an action and a page type
		 *
		 * @param string $action
		 * @param string $pageType
		 *
		 * @return bool
		 */
		'can' => function ($action, $pageType) {
			if (empty($action) || empty($pageType)) {
				return false;
			}

			$roleId = $this->role()->id();

			if ($roleId === 'nobody') {
				return false;
			}

			$pageBlueprint = Blueprint::load('pages/' . $pageType);

			$requestedPageAction = $pageBlueprint['options'][$action] ?? null;

			// evaluate the blueprint options block
			if (isset($requestedPageAction) === true) {
				if ($requestedPageAction === false) {
					return false;
				}

				if ($requestedPageAction === true) {
					return true;
				}

				// requested page permission for matching user role
				if (isset($requestedPageAction[$roleId])) {
					return (bool) $requestedPageAction[$roleId];
				}

				// requested page permission for all roles
				if (isset($requestedPageAction['*'])) {
					return (bool) $requestedPageAction['*'];
				}
			}

			// user's global page permission
			return $this->role()->permissions()->for('pages', $action);
		}
	]
]);

Yes, for now it would have been the easiest to check for the role name and be done with it. But I have a feeling that for my project the overhead is worth it, because the roles/permission/requirements are not very well defined yet. So I’d rather code with the actual permissions and save future me/coders some head scratching.