Statistics
893
6
2
187d
Badges

Carrooi/Security

Build Status Donate

Extensible authorization built on top of nette/security.

This package came in handy if you want to create modular website and keep all pieces decoupled with "custom" checking for privileges.

Now you can really easily check if eg. given user is author of some book and so on..

This idea comes from nette/addons website.

Installation

$ composer require carrooi/security
$ composer update

Then just enable nette extension in your config.neon:

extensions:
	authorization: Carrooi\Security\DI\SecurityExtension

Configuration

extendsions:
	authorization: Carrooi\Security\DI\SecurityExtension

authorization:
	default: true

	resources:
		book:
			default: false
			
			actions:
				view: true
				add:
					loggedIn: true
				edit:
					roles: [admin]
				delete:
					roles: [admin]

Well, there is nothing modular.... Yet.... We just say that resource book has view action which is accessible to everyone, add to logged users and edit with delete actions to users with admin role.

There are also two default options. With the first one we say that each ->isAllowed() call on unknown action will automatically return true. But the second default will overwrite this option for all book actions to false.

That means that eg. ->isAllowed('book', 'detail') will return false, but ->isAllowed('user', 'detail') true.

Other resources and actions

If default option is not enough, you can create default resource or default action with asterisk.

authorization:
	
	resources:
		favorites:
			actions:
				*:
					loggedIn: true

Custom resource authorizator

Now lets create the same authorization for books by hand.

services:

	- App\Model\Books

authorization:
	resources:
		book: App\Model\Books

App\Model\Books must be registered service.

namespace App\Model;

use Carrooi\Security\Authorization\IResourceAuthorizator;
use Carrooi\Security\User\User;

/**
 * @author David Kudera
 */
class Books implements IResourceAuthorizator
{


	/**
	 * @return array
	 */
	public function getActions()
	{
		return [
			'view', 'add', 'edit', 'delete',
		];
	}


	/**
	 * @param \Carrooi\Security\User\User $user
	 * @param string $action
	 * @param mixed $data
	 * @return bool
	 */
	public function isAllowed(User $user, $action, $data = null)
	{
		if ($action === 'view') {
			return true;
		}
		
		if ($action === 'add' && $user->isLoggedIn()) {
			return true;
		}

		if (in_array($action, ['edit', 'delete']) && $user->isInRole('admin')) {
			return true;
		}

		return false;
	}

}

You can also return * from getActions() method to tell that the authorizator can accept any action.

Use objects as resources

In previous code you may noticed unused argument $data in isAllowed method. Imagine that you want to allow all users to update or delete their own books. First thing you need to do, is register some kind of "translator" from objects to resource names (lets say mappers).

authorization:
	targetResources:
		App\Model\Book: book

Now every time you pass App\Model\Book object as resource, it will be automatically translated to book resource, which will be then processed with your App\Model\Books service registered in previous example.

namespace App\Presenters;

use Nette\Application\BadRequestException;
use Nette\Application\ForbiddenRequestException;

/**
 * @author David Kudera
 */
class BooksPresenter extends BasePresenter
{

	// ...

	/**
	 * @param int $id
	 * @throws \Nette\Application\BadRequestException
	 * @throws \Nette\Application\ForbiddenRequestException
	 */
	public function actionEdit($id)
	{
		$this->book = $this->books->findOneById($id);
		if (!$this->book) {
			throw new BadRequestException;
		}
		if (!$this->getUser()->isAllowed($this->book, 'edit')) {
			throw new ForbiddenRequestException;
		}
	}

}

// ...
class Books implements IResourceAuthorizator
{

	// ...
	public function isAllowed(User $user, $action, $data = null)
	{
		// ...

		if (
			in_array($action, ['edit', 'delete']) &&
			$data instanceof Book && 
			(
				$user->isInRole('admin') ||
				$data->getAuthor()->getId() === $user->getId()
			)
		) {
			return true;
		}

		return false;
	}

}

Or you can write "magic" is<action>Allowed methods:

class Books implements IResourceAuthorizator
{

	public function isAllowed(User $user, $action, $data = null)
	{
		return false;
	}
	
	public function isEditAllowed(User $user, $data = null)
	{
		return true;
	}

}

Linking to presenter

class BasePresenter extends Nette\Application\UI\Presenter
{

	use Carrooi\Security\Authorization\TPresenterAuthorization;
	
	public function checkRequirements($element)
	{
		if ($element instanceof Nette\Reflection\Method) {
			if (!$this->checkMethodRequirements($element)) {
				throw new Nette\Application\ForbiddenRequestException;
			}
		}
	}

}

Now you can simply use annotations for setting current resource and action

class BookPresenter extends BasePresenter
{

	/**
	 * @resource(book)
	 * @action(view)
	 */
	public function actionDefault()
	{

	}

}

Securing presenter components and signals

You can restrict any component or signal to some action. With that no one can access for example edit form from add action.

class BasePresenter extends Nette\Application\UI\Presenter
{

	use Carrooi\Security\Authorization\TPresenterAuthorization;
	
	public function checkRequirements($element)
	{
		// ...
	}
	
	protected function createComponent($name)
	{
		$this->checkComponentRequirements($name);
        return parent::createComponent($name);
	}

}
class BookPresenter extends BasePresenter
{

	/**
	 * @action(edit)
	 */
	protected function createComponentEditForm()
	{
		
	}
	
	/**
	 * @action(default, detail)
	 */
	protected function createComponentFavoriteButton()
	{
	
	}
	
	/**
	 * @action(*)
	 */
	protected function createComponentReadLaterButton()
	{
	
	}

}

Keep in mind that actions at components or signals are presenter actions, not actions at your authorization configuration.

Now editForm component can be rendered only on edit action, favoriteButton only on default or detail actions and readLaterButton anywhere.

Same @action annotations can be used also for signals.

Presenter security modes

By default this package will try to check action, render, handle and createComponent methods. But if you'll omit some annotations, nothing will happen and that method will be allowed. This can be changed by turning on strict mode.

authorization:
	actions: strict
	signals: strict
	components: strict

Other options are true or false, where true is default value.

Compiler extension

Your own DI compiler extensions can implement interface Carrooi\Security\DI\ITargetResourcesProvider for resource mappers.

namespace App\DI;

use Carrooi\Security\DI\ITargetResourcesProvider;
use Nette\DI\CompilerExtension;

/**
 * @author David Kudera
 */
class AppExtension extends CompilerExtension implements ITargetResourcesProvider
{


	/**
	 * @return array
	 */
	public function getTargetResources()
	{
		return [
			'App\Model\Book' => 'book',
		];
	}

}

Extending User class

Be carefull if you want to extend Nette\Security\User class, because carrooi\security already extends that class for it's own needs.

Changelog

  • 2.0.0

    • Allow resources to have many custom authorizators #9 (BC break)
  • 1.2.1

    • Throw an exception when using not registered resource object #8
  • 1.2.0

    • Add support for interfaces at target authorizators #5
    • Refactored tests #2
    • Remove support for magic presenter resource getters #7
    • Fix magic resource authenticator with private is<action>Allowed #4
  • 1.1.0

    • Add support for magic resource authenticator methods is<action>Allowed
  • 1.0.3

    • Add lazy register resource authorizators - prevents circular references in some cases
  • 1.0.2

    • Looking if given object for authorization is subclass of some registered target resource
  • 1.0.1

    • Added default resources and actions (asterisk)
  • 1.0.0

    • Initial commit

No release at this moment. Try to create first one.

Is this addon outdated? Did you find an issue? Please report it.

Componette Componette admin@componette.com