Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RBAC module #1

Open
jelen07 opened this issue Jun 28, 2022 · 1 comment
Open

RBAC module #1

jelen07 opened this issue Jun 28, 2022 · 1 comment

Comments

@tg666
Copy link
Contributor

tg666 commented Jun 29, 2022

Model

New aggregates:

<?php

namespace SixtyEightPublishers\UserBundle\Domain\Role;

class Role implements AggregateRootInterface
{
    use DeletableAggregateRootTrait;
    
    private RoleId $id;

    private Name $name;

    /** RoleHasParent[] */
    private Collection $parents;

    private Resources $resources;

    // the role cannot be deleted if the property is set to TRUE
    private bool $persistent;

    // the role has automatically access to everything if the property is set to TRUE, Resources
    private bool $admin;
}
<?php

namespace SixtyEightPublishers\UserBundle\Domain\Role;

/** Association between Role and Role */
class RoleHasParent
{
    private string $id;

    private Role $role;

    private RoleId $parentId;
}
<?php

namespace SixtyEightPublishers\UserBundle\Domain\User;

/** Association between User and Role */
class UserHasRole
{
    private string $id;

    private User $$user;

    private RoleId $roleId;
}

The object Resources can be just a value object that contain named resources:

<?php

namespace SixtyEightPublishers\UserBundle\Domain\Role\ValueObject;

final class Resources
{
    /** Resource[] */
    private array $resources = [];

    public function withPrivilege(string $resourceName, string $privilege): self {}

    public function withoutPrivilege(string $resourceName, string $privilege): string {}

    public function equals(self $resources): bool {}
}

final class Resource
{
    private string $name;

    /** string[] */
    private array $privileges = [];

    public function withPrivilege(string $privilege): self {}

    public function withoutPrivilege(string $privilege): string {}

    public function equals(self $resource): bool {}
}

The value object will be stored in the database as JSON like:

[
  {
    "name": "projects",
    "privileges": [
      "read"
    ]
  },
  {
    "name": "categories",
    "privileges": [
      "read",
      "write"
    ]
  }
]

Currently, the User aggregate contains a property private Roles $roles that is basically an array of role names. But the property will be now changed to a collection of UserHasRole entities.
So all QueryHandlers for Users must be modified. They will must fetch all roles (their IDs and names) and map it to the Views.

An application resources & privileges

Each application has different resources and privileges so the resources must be provided by an application or an administration module (in the future).

<?php

namespace SixtyEightPublishers\UserBundle\Application\Acl;

interface ResourceInterface
{
    public static function name(): string;

    public function privileges(): array;
}

abstract class AbstractResource implements ResourceInterface
{
    private ?array $privileges = NULL;

    public function privileges(): array
    {
        if (NULL === $this->privileges) {
            $reflection = new ReflectionClass(static::class);
            $this->privileges = array_values($reflection->getConstants());
        }

        return $this->privileges;
    }
}

In the application:

<?php

final class ProjectResource extends AbstractResource
{
    public const READ = 'read';
    public const WRITE = 'write';
    public const DELETE = 'delete';

    public static function name(): string
    {
        return 'project';
    }
}
services:
    - ProjectResource

All resources can be injected into the services by array of type ResourceInterface

Authorizator

<?php

namespace SixtyEightPublishers\UserBundle\Application\Acl;

interface AuthorizatorInterface
{
    public function isAllowed(array $roles, string $resource, string $privilege): bool;
}

final class Authorizator implements AuthorizatorInterface
{
    public function addRole(string $name, array $parents = []): void {}

    public function addResource(ResourceInterface $resource): void {}

    public function allow(string $role, string $resource, array $privileges = []): void {}

    public function isAllowed(array $roles, string $resource, string $privilege): bool
    {
         # resolve
    }
}

final class LazyAuthorizator implements AuthorizatorInterface
{
    private QueryBusInterface $queryBus;

    private ?AuthorizatorInterface $inner = NULL;

    public function __construct(QueryBusInterface $queryBus)
    {
        $this->queryBus = $queryBus;
    }

    public function isAllowed(array $roles, string $resource, string $privilege): bool
    {
         return $this->getInner()->isAllowed($roles, $resource, $privilege);
    }

    private function getInner(): AuthorizatorInterface
    {
        if (NULL !== $this->inner) {
            return $this->inner;
        }

         $a = new Authorizator();

         # fill the authorization

        return $this->inner = $a;
    }
}

The "generated" Authorizator should be cached in the future.

Nette bridge:

<?php

namespace SixtyEightPublishers\UserBundle\Bridge\Nette\Acl;

final class Authorizator implements Nette\Security\Authorizator
{
    private AuthorizatorInterface $authorizator;

    public function __construct(AuthorizatorInterface $authorizator)
    {
         $this->authorizator = $authorizator;
    }

    public function isAllowed($role, $resource, $privilege): bool
    {
         return $this->authorizator->isAllowed([(string) $role], (string) $resource, (string) $privilege);
    }
}

Usage

<?php

$container->getByType(AuthorizatorInterface::class)->isAllowed(['admin'], ProjectResource::name(), ProjectResource::DELETE);

$container->getByType(Nette\Security\Authorizator::class)->isAllowed('admin', ProjectResource::name(), ProjectResource::DELETE);

$user = $container->getByType(Nette\Security\User::class);

$user->isAllowed(ProjectResource::name(), ProjectResource::DELETE);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants