ZF2 - BjyAuthorize - How to Get Rules and Guards f

2019-03-20 19:55发布

问题:

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?

回答1:

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'
        )
)
  • (cont) I put it under guards as opposed to rule_providers, because what we are dealing with here is not a pure rule provider. It is a guard provider, or "an adapter that gets rules out of your ObjectRepositoryProvider, and injects them into controller guard". This factory should look similar to this, except that you will be loading rules, not roles. You will then be injecting the rules into Controller, as in the next step.
  • Inject the rules into Controller, very much like it is done here

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.


回答2:

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.