ZF2 - BjyAuthorize - How to Get Rules and Guards from a Database
Asked Answered
R

2

5

I'm using BjyAuthorize with Zend Framework2 to implement authorization and was able to successfully integrate roles from database. Now I want to get my Rules and Guards also from data base tables. How can I do this?

Rufus answered 16/8, 2013 at 2:0 Comment(1)
^^ this question is really about how to navigate "Configuration Hell" of Zend Framework, and especially, how to navigate "Configuration Hell" of BjyAuthorize module for Zend framework. "Configuration Hell" is what new users typically battle when they begin trying out ZF2 and ZF2 modules. Code is easy, Understanding what goes where inside the Configuration and how it all works, while also learning all the OO concepts of ZF2 and modules that are used, is hard. My suggestion in general is to make little configuration change steps at a time and test them thoroughly before moving on further.Ellaelladine
E
7

The easiest method and "the trick" here is really to:

  1. Get your rules and guards into the same array format as it is shown in example configuration. So after reading records from the database, in whatever format your raw database data is, process it to match the same guard format as in the configuration. (My answer goes into detail on how to do that with Doctrine ORM, but also should give you an idea with other DB engines. Just substitute "DB read" operation with your fave database engine)

  2. Inject the rules that are already in the proper format BjyAuthorize expects (because you made them so), into BjyAuthorize\Guard\Controller, from within YOUR_MODULE_NAME\Factory\DoctrineControllerGuardAdapterFactory, which you will write. Bjy's Controller will treat the rules as if those rules came from configuration*, and not suspect any difference.

  3. Step back and enjoy!

This is the construct that you need to write in your own module:

namespace YOUR_MODULE_NAME\Factory;

/**
 * See "How and where exactly to register the factory" in ZF2 config
 * below in my answer.
 */
class [Custom]ControllerGuardAdapterFactory 
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        /**
         * Retrieve your rules from favorive DB engine (or anything)
         *
         * (You may use $serviceLocator to get config for you DB engine)
         * (You may use $serviceLocator to get your DB engine)
         * (You may use $serviceLocator to get ORM Entity for your DB engine)
         * (Or you may hack your DB connection and retrieval in some other way)
         *
         * Hell, you may read them from a text file using PHP's file() method
         * and not use $serviceLocator at all
         *
         * You may hardcode the rules yourself right here for all that matters
         */            
        $rules = ... //array(...);


        /** 
         * Inject them into Bjy's Controller
         *
         * Rules must be in the same format as in Bjy config, or it will puke.
         * See how ['guards'][\BjyAuthorize\Guard\Controller::class] is constructed 
         * in Bjy configuration for an example
         */             
        return new \BjyAuthorize\Guard\Controller($rules, $serviceLocator); 
    }
}

Now watch and observe how mind-numbingly complicated this can be made! (modeled after Bjy's own mechanisms)

This is mostly just ZF2, OO & Bjy "Configuration Hell", folks, nothing special otherwise. Welcome to ZF2 and Bjy and ORM Configuration Hell. You are welcome.

Detailed Answer - How to Implement?

Write an adapter factory, which reads rules from database, and then injects them into BjyAuthorize's Controller Guard. The effect will be the same as if the rules were being read from ['guards'][\BjyAuthorize\Guard\Controller::class]

What?

The way BjyAuthorize's Controller Guard works is it takes rules in a certain format (format specified for ['guards']['BjyAuthorize\Guard\Controller']), and then it uses the rules to populate the ACL. It also computes Resources from rules for you and loads those into ACL as well. If it didn't, you would have to write your own Resource Provider to do so.

So the task becomes:

  • Load rules from database and Transform the rules to format BjyAuthorize expects. This can be done in your own Rule Provider, much like this one.
  • You can use a factory to load your particular DB and storage class configuration arrays from module.config.php file. I put mine under ['guards']['YOUR_MODULE_NAME_controller_guard_adapter'].
'guards' => array(
        'YOUR_MODULE_NAME_controller_guard_adapter' => array(
            'object_manager' => 'doctrine.entity_manager.orm_default',
            'rule_entity_class' => 'YOUR_MODULE_NAME\Entity\ObjectRepositoryProvider'
        )
)

Example Implementation Details (from Q/A in comments)

More on the last point of "Injecting rules into Controller". Basically two steps: 1) make sure you already have (or will) generate your rules somehow (that's the hard step ). 2) inject those rules into controller (that's the easier step). The actual injection is done like this

$rules = __MAGIC__;  //get rules out of somewhere, somehow.
return new Controller($rules, $serviceLocator); //$rules injection point

See code block below for my own implementation, where the last line in the block is the line I gave just above here.

namespace YOUR_MODULE_NAME\Factory;

use BjyAuthorize\Exception\InvalidArgumentException;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use YOUR_MODULE_NAME\Provider\Rule\DoctrineRuleProvider;    //this one's your own
use BjyAuthorize\Guard\Controller;

class DoctrineControllerGuardAdapterFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        //just setting up our config, move along move along...
        $config = $serviceLocator->get('Config');
        $config = $config['bjyauthorize'];

        //making sure we have proper entries in our config... 
        //move along "nothing to see" here....
        if (! isset($config['guards']['YOUR_MODULE_NAME_controller_guard_adapter'])) {
            throw new InvalidArgumentException(
                'Config for "YOUR_MODULE_NAME_controller_guard_adapter" not set'
            );
        }

        //yep all is well we load our own module config here
        $providerConfig = $config['guards']['YOUR_MODULE_NAME_controller_guard_adapter'];

        //more specific checks on config
        if (! isset($providerConfig['rule_entity_class'])) {
            throw new InvalidArgumentException('rule_entity_class not set in the YOUR_MODULE_NAME guards config.');
        }

        if (! isset($providerConfig['object_manager'])) {
            throw new InvalidArgumentException('object_manager not set in the YOUR_MODULE_NAME guards config.');
        }

        /* @var $objectManager \Doctrine\Common\Persistence\ObjectManager */
        $objectManager = $serviceLocator->get($providerConfig['object_manager']);

        //orp -- object repository provider
        //here we get our class that preps the object repository for us
        $orp=new DoctrineRuleProvider($objectManager->getRepository($providerConfig['rule_entity_class']));

        //here we pull the rules out of that object we've created above
        //rules are in the same format BjyAuthorize expects
        $rules=$orp->getRules();

        //here pass our rules to BjyAuthorize's own Guard Controller.  
        //It will not know the difference if we got the rules from Config or from Doctrine or elsewhere,  
        //as long as $rules are in the form it expects.
        return new Controller($rules, $serviceLocator); 
    }
}

DoctrineRuleProvider

namespace YOUR_MODULE_NAME\Provider\Rule;

use Doctrine\Common\Persistence\ObjectRepository;
use BjyAuthorize\Provider\Rule\ProviderInterface;

/**
 * Guard provider based on a {@see \Doctrine\Common\Persistence\ObjectRepository}
 */
class DoctrineRuleProvider implements ProviderInterface
{
    /**
     * @var \Doctrine\Common\Persistence\ObjectRepository
     */
    protected $objectRepository;

    /**
     * @param \Doctrine\Common\Persistence\ObjectRepository $objectRepository            
     */
    public function __construct(ObjectRepository $objectRepository)
    {
        $this->objectRepository = $objectRepository;
    }

    /**
     * Here we read rules from DB and put them into an a form that BjyAuthorize's Controller.php understands
     */
    public function getRules()
    {
        //read from object store a set of (role, controller, action) 
        $result = $this->objectRepository->findAll();

        //transform to object BjyAuthorize will understand
        $rules = array();
        foreach ($result as $key => $rule)
        {
            $role=$rule->getRole();
            $controller=$rule->getController();
            $action=$rule->getAction();            

            if ($action==='all')    //action is ommitted
            {
                $rules[$controller]['roles'][] = $role;
                $rules[$controller]['controller'] = array($controller);
            }
            else
            {
                $rules[$controller.':'.$action]['roles'][]=$role;
                $rules[$controller.':'.$action]['controller']=array($controller);
                $rules[$controller.':'.$action]['action']=array($action);
            }                       
        }    

        return array_values($rules);
    }
}

Q: How and where exactly to register the factory DoctrineControllerGuardAdapterFactory

A: Try this path: module\YOUR_MODULE_NAME\config\module.config.php and have

'service_manager' => array(
    'factories' => array(
        'YOUR_MODULE_NAME_controller_guard_adapter' => \YOUR_MODULE_NAME\Factory\DoctrineControllerGuardAdapterFactory::class
    )
)
  • Note: YOUR_MODULE_NAME. The thing on the left of => sign is "the key", and can be anything you want it to be. Convention in Bjy is that it is similar to the actual class names and paths. And the thing on the right of the => is the actual fully qualified namespace to the class that you want to call with with this key.
Ellaelladine answered 17/1, 2014 at 23:0 Comment(16)
Since the navigation menu items are read from 'rule_providers' and not from 'guards', do I need to develop a separate factory for that. And I’m not using Doctrine and how to set ‘object_manager’ in guards array to work without Doctrine.Rufus
hmm... navigational menu should be separate from rule_providers.., so I don't really get the question. It has been a while though since I've dealt with this. It won't hurt to do have separate factory by the way :) If you find them to be too similar you can refactor your code later on. (aka, don't get too hung up on how ZF2 does things, use it more as a set of guidelines, and not a rigid set of rules.)Ellaelladine
Hi Dennis, thanks for the explanation it got me started but could you elaborate on how to inject rules into the controller?Matthus
I want to do this without using Doctrine. And also I should be able to replace "YOUR_MODULE_NAME" with BjyAuthorize if I create the classes inside BjyAuthorize folder, right?Rufus
if I understood you right - no, do not alter code inside the vendor folder, including BjyAuthorize folder. Those are libraries intended for you to use but not alter. I assume you want to use Zend\Db. The general steps with ZF2 and Bjy are - 1) set up config in your module, 2) use config to locate and use your DB and read rules from db, 3) inject the rules from step 2 into Bjy's controller. If any of those steps give you trouble, feel free to ask a new questionEllaelladine
I would like to know how can I implement the Entity to use it? I read a lot the questions but did not get it well.Attaboy
Ok, you need to create an Entity, maybe call Rule, then an ObjectRepositoryProvider that implements ProviderInterface and at the ObjectRepositoryProvider I need to return the array with the same arranger that the default implements? Thanks.Attaboy
sounds to me like you are talking about a class I called DoctrineRuleProvider, and yes. See new editEllaelladine
@Dennis, now my module.config looks like it: 'My_MODULE\Factory\DoctrineControllerGuardAdapterFactory' => array( 'object_manager' => 'doctrine.entity_manager.orm_default', 'rule_entity_class' => 'My_MODULE\Entity\Rule' ) I created the Entity Rule and add controller, action, roles based on the code you wrote. But now I get this error: Fatal error: Uncaught exception 'Zend\EventManager\Exception\InvalidArgumentException' with message 'Zend\EventManager\EventManager::attach: expects a callback; none provided. Can you give me one more help? Thanks.Attaboy
@Ellaelladine could you give one more help on it? I googled about the situation but get nothing as answer to it, thanks.Attaboy
since ZF2 is complex and has many moving parts .. I am not immediately sure what could be wrong. It may help to out a echo '<pre>';debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);echo'</pre>' inside your code right before the error happens. Look for any methods called attach in your code that you find by backtrace or otherwise. Those expect an argument of 'callable' type. You either have code that passes nothing, or something of a type that is not 'callable'.Ellaelladine
Ok, I will let it for now! Maybe one day you could add to your github a sample, using zfcuser, bjyauthorize and reading the rules from database. Thanks.Attaboy
I may try but not right now. What I have now needs some work before I can post it. But the question is .. will my finished result help you? ZF2 development is full of errors like you have mentioned, especially if you are starting out. It is a pain.Ellaelladine
How and where exactly to register the factory DoctrineControllerGuardAdapterFactory?Acton
@Ellaelladine is the cache provide by bjyauthorize can be used or should implement my own?Roble
sorry.. don't have a clue here. Try asking github.com/bjyoungblood/BjyAuthorize/issuesEllaelladine
J
1

Basically you have to write your own Provider.

Check out the different RoleProvider. Every RoleProvider implements the Provider\Role\ProviderInterface. The same thing has to be done when you want to implement Guards and Rules. You go into the specific directories Provider\Rule and Provider\Resource and check for the specific ProviderInterface.

That way you can write your own class implementing the Interface and then via configuration you tell BjyAuthorize to use your provider-classes.

As far as Guards are concerned, i do believe it is not yet possible to create those from Database. You would have to modify / PR the Module itself to make that happen.

Jeramie answered 16/8, 2013 at 6:44 Comment(2)
Thank you Sam for the quick answer. Since I cannot manage without Guards and if I comment them, I get 403 Forbidden on those pages, I really don't know how to go on without creating them from database. Any more ideas on modifying the Module to make it happen?Rufus
As of version 1.4.0, if you are relying on guards, you need to specify them either via Config, or via something else. The something else (i.e. guards provider) is not implemented in a known public repository.Ellaelladine

© 2022 - 2024 — McMap. All rights reserved.