V9.0 permissions

Has the way permissions work in v9 changed? I have a custom permission key (JtFPlugin) I use across my addons.
Within the addons, when I try and save a permission assignment is failing on \Concrete\Controller\Permissions\Categories\TaskHandlers\JtfPlugin

Searching /TaskHandlers/ reveals many core classes relating to that.

Can you outline what the changes are to help me refactor.

Decided to see what the core was doing for other advanced permissions. Setting/clearing in task permissions I ran into a bug. an infinite jQuery error to do with missing easing within an animation, so the animation keeps on running forever.

I have added a pull request to fix the easing issue

However, I am still unable to get a custom permission to work. I don’t think my problem is with the TaskHandlers. Adding a minimal TaskHandler that checks task permissions resolved that problem.

Now my custom permission save appears to complete, but nothing is actually saved. The code continues to work in 8.5.5. Hence my top suspect is that something is different in the way custom permissions work in 9.0.0rc.

This is definitely related to our removal of “tools” support in version 9. Tools were mostly gone in the core and in packages (in favor of routes, controllers and views) over the past several years, but one place where they showed up a lot and we only recently removed them in the v9 branch was in permissions.

Tools were used not only for showing the permissions dialogs at various points, but also for saving the permissions themselves. The TaskHandlers namespace you’re seeing is a refactoring of that (which I believe @mlocati did in a really nice way).

What you’re going to want to do is create a class named Concrete\Package\JtfPlugin\Controller\Permissions\Categories\TaskHandlers\YourPermissionCategoryHandle in a file at packages/jtf_plugin/controllers/permissions/categories/task_handlers/your_permission_category_handle.php that extends a permission object task handler, and contains any logic for grabbing the permission object from the request.

Here’s an example from our Multiple Step Workflow add-on. In this addon, we apply custom permissions to an object named “Workflow Step” and we’ve created a custom permission category named “multiple_step_workflow_step” in order to hold those custom permissions against the step objects. Here’s what that file looks like.


	namespace Concrete\Package\MultipleStepWorkflow\Controller\Permissions\Categories\TaskHandlers;

	use Concrete\Core\Entity\Board\Board as BoardEntity;
	use Concrete\Core\Error\UserMessageException;
	use Concrete\Core\Http\ResponseFactoryInterface;
	use Concrete\Core\Page\Page;
	use Concrete\Core\Permission\Access\Access;
	use Concrete\Core\Permission\Category\ObjectTaskHandler;
	use Concrete\Core\Permission\Checker;
	use Concrete\Core\Permission\Key\Key;
	use Concrete\Core\Permission\ObjectInterface;
	use Concrete\Core\View\View;
	use Concrete\Package\MultipleStepWorkflow\Workflow\Step;
	use Doctrine\ORM\EntityManagerInterface;
	use Symfony\Component\HttpFoundation\Response;

	defined('C5_EXECUTE') or die('Access Denied.');

	class MultipleStepWorkflowStep extends ObjectTaskHandler
		 * {@inheritdoc}
		 * @see \Concrete\Core\Permission\Category\ObjectTaskHandler::$hasWorkflows
		protected $hasWorkflows = true;

		 * {@inheritdoc}
		 * @see \Concrete\Core\Permission\Category\ObjectTaskHandler::getPermissionObject()
		protected function getPermissionObject(array $options): ObjectInterface
			$workflowPage = Page::getByPath('/dashboard/system/permissions/workflows/steps');

			$stepID = $options['mwsID'] ?? null;
			$step = Step::getByID($stepID);
			if ($step === null) {
				throw new UserMessageException(t('Invalid workflow step object'));
			$cp = new Checker($workflowPage);
			if (!$cp->canViewPage()) {
				throw new UserMessageException(t('Access Denied.'));

			return $step;

		protected function displayAccessCell(ObjectInterface $permissionObject, array $options): ?Response
			$pk = Key::getByID($options['pkID']);
			$view = new View('/backend/permissions/labels');

			$this->set('pk', $pk);
			$this->set('pa', Access::getByID($options['paID'], $pk));

			$content = $view->render();

			return $this->app->make(ResponseFactoryInterface::class)->create($content);

I know this is kind deep in the weeds, but hopefully this helps.

1 Like

For anyone else running into this, in addition to the above, the Assignment class needs to provide

public function getPermissionKeyTaskURL(string $task = '', array $options = []): string

public function getPermissionKeyToolsURL($task = false)