Cakephp 4 Authorization

What is the best method for authorizing only by action and not by object or entity? Example, I have an action that touches several models that that user may not normally get access to. It seems like the new authorization model focus on tables/entity/queries etc…

I wonder if this is what you’re looking for.

I use it to control which user/roles have access to which controller/actions

Thank you, I missed that section in the Book.

I have also found a way to keep my existing isAuthorized functions in my controllers. I created a CustomResolver implementing the Resolver interface.This will return one policy in the getPolicy() function. This policy will then do $resource->isAuthorized($user);

I’ve found the policies very adaptable. Good deal, getting your isAuthorized policies running!

Do you have an example of this?

I’m assuming you’re placing $resource->isAuthorized($user); in the canAccess method of the RequestPolicy? But what is $resource (I would assume the controller), but where does that come from?

Any one have an idea of how this Resolver is written? Interested in the idea of executing my isAuthorized methods as well.

Yes, this solution works for me.

TitanResolver.php:
class TitanResolver implements ResolverInterface{

    public function getPolicy($resource)
    {
        $class = App::className('App\Policy\TitanPolicy');
        return new $class();
    }


}

TitanPolicy.php:

use Authorization\Policy\BeforePolicyInterface;

class TitanPolicy implements BeforePolicyInterface
{
    public function before($user, $resource, $action)
    {
        return $resource->isAuthorized($user);
    }
}

So when I use this, I get an exception that my page didn’t apply any authorization checks. I have the following code in my Application.php file. Not sure if it’s right or not. Not sure what I’m missing.

        public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
    {
        $mapResolver = new MapResolver();
        $mapResolver->map(TitanResolver::class, TitanPolicy::class);

        return new AuthorizationService($mapResolver);
    }

@justinkbug, the setup is a little more straightforward than you are imagining. Here is a mapping from my own code:

        $mapResolver->map(IdPackage::class, IdAccessPolicy::class);

with this in place I can check policies anywhere in my app. I just need three things

  1. the Identity object for the Authenticated user, wrapped in an Authorization\IdentityDecorator

  2. an object of the type named as the first argument of my map() call
    (in this case an IdPackage)

  3. a class of the type named as the second argument of my map() call
    (in this case an IdAccessPolicy)

The Identity object should be available and correct if both the Authentication and Authorization plugins are installed properly.

Here is some code that would work:

// assuming $identity is a decorated Identity object
$IdPackage = new IdPackage($tenant_id, 'tenant')
if ($identity->can('edit', $IdPackage) {
  //more code here
}

The IdPackage in will map to IdAccessPolicy as defined in the earlier map() call. And based on this code, we would expect that class to look something like this:

class IdAccessPolicy {

 public function canEdit($identity, $IdPackage) {
   //access logic here
   // return boolean
 }
}

In this way you can easily map any object type to one policy class and implement any kind of policy check you like. As illustrated here, if no naturally occurring object exists that has the data relevant to your needs, you can design one and map to it.

You might also want to look at the Request Authorization Middleware which can authorize each controller/action request before any controller is constructed.

Also…

My Application.php looks like this:

 public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
    {
        $resolver = new TitanResolver();

        return new AuthorizationService($resolver);
    }

I also have this line in my AppController.php initialize() function

public function beforeFilter(EventInterface $event){
        //identity is set when user logs in
        if($this->identity){
            $this->Authorization->authorize($this);
       }
}

This seems to work okay. The authorization is then called for any function.

I’m completely new at CakePHP, but started my career in 1976. So please be kind if there is something cake-ish that does exactly what I’m proposing that I haven’t found yet. I’m “biting the bullet” and learning CakePHP so I can continue developing new apps.

The question was the “BEST method for authorizing ONLY by action.” Here is how I have done it (using perl, C/C++, Python, etc, etc). I figured I’d build out a cake-ish version of the auth code (sooner than later).

  1. I kept a table called UserFunctionMatch that looked something like this:
    username VARCHAR(255) NOT NULL,
    funcname VARCHAR(255) NOT NULL,
    perms VARCHAR(255) NOT NULL DEFAULT ‘guest’,
    PRIMARY KEY (username,funcname);

  2. Every “function” (user, blog, admin, etc – think of it in any way your app makes sense, but probably something like a high-level menu) would have an entry.
    If “fred” is logged in, and (somehow) has a URL that is secured (/admin/users), then I’d check for an entry under fred/admin to see if fred has access to users, then fred/users to see what fred can do there. If there is no entry in the table, fred has zero rights, so I flag the security breach and warn the user. If there is an entry, I then check to see if the perms field specifies this function.
    Example of fred/admin perms field: “users editmine dobackup”
    Example of fred/users perms field: “view list activate inactivate editmine”
    Obviously with the url in question, the check would be for something like “add” or “adduser” (you know what it needs to be from the code, right?). If it fails, we flag the security breach and warn the user. If it succeeds, then we allow the function to continue.
    The default for the perms field is ‘guest,’ which just tells you they are not specifically allowed to do anything else.
    As an aside, I also had ‘superuser’ and ‘admin’ that had meanings you’d expect.

  3. The beauty thing about this is that you can add a new function to the application (import/update users from LDAP, for instance) with impunity, and just add the (few) records to the table for the folks who have access to it, and voila, it worked.

The application I’m starting on right now is a real estate management interface. Gathering information from the various states and counties, digesting it into a usable form, then allowing franchise owners to pull out the data they need. It will also manage properties, including a variety of ticketing services for property managers to use to send assignments to contractors. Right now I’m looking at around 125 unique functionality groups, and will probably built out to around 5k users, including franchise owners, property managers, employees, vendors, and contractors.

So I need something that will be comprehensive, easy to manage, and completely secure. The dealio I outlined is something I’ve used for three decades, so I’m pretty sure it can handle what I need. So to that extent, this answers the query on a best-practices for doing action-based authentication.

If, however, there is already something cake-ish that does the same thing, I’d LOVE some pointers. I’m always open to learning new stuff…

If there’s not, I’d LOVE some suggestions on how y’all would tackle it using the CakePHP environment. If there is enough interest, I might just push it to a project – which, obviously, would require it being completely integratable with all-things-cake-ish. :smiley:

DL

I’ll let the ones that are more experienced with CakePHP4 comment on that.

I did get my authorization working using my already existing isAuthorized methods (from CakePHP2), using some of the examples from above. So far this seems to work. I basically used the request authorization middleware approach that was suggested and have the following code as part of the RequestPolicy.

public function canAccess($identity, ServerRequest $request)
{
    $params = $request->getAttribute('params');
    if (isset($params['prefix']) && $params['prefix'] === 'Admin') {
        $class = 'App\Controller\Admin\\' . $request->getParam('controller') . 'Controller';
    } else {
        $class = 'App\Controller\\' . $request->getParam('controller') . 'Controller';
    }
    $class = App::className($class);
    $controller = new $class;
    return $controller->isAuthorized($identity);
}

The only thing I’ve noticed is that the canAccess method gets run even if I"m not authenticated. I was originally under the impression that authentication ran first and then authorization. However, that doesn’t seem to be the case. At least not in my situation. Can anyone verify that? I guess all I need to do is check to see if $identity exist or not.

Ok, well, that’s not going to work. I need authentication to do it’s job before authorization is even considered. Going to have to figure out what I’ve done wrong.

What do you mean by “authentication to do it’s job before authorization is even considered”? In the case of someone that’s not logged in, authentication has arguably done it’s job; it’s confirmed that there is nobody logged in. Allowing people who aren’t logged in access to some resources is a common requirement.

I have code like this in a number of policies where there is never any unauthenticated access allowed:

public function before($identity, $resource, $action) {
    if (!$identity) {
        throw new ForbiddenException();
    }
}

Ok, but if someone isn’t authenticated, don’t you want them to be redirected to the login page? That’s the problem I’ve ran into. Instead of an unauthenticated person getting redircted to the login page, authorization running and throwing a ForbiddenException instead. That’s what I mean by authentication running first, before authorization.

I even did what the documentation recommend and put it in the following order.

    // Authentication and Authorization
    // Authentication should always be called before Authroization.
    $middlewareQueue->add(new AuthenticationMiddleware($this));
    $middlewareQueue->add(new AuthorizationMiddleware($this));

There are very often resources that people who are not logged in should still have access to. At the very least, the login function itself. So rejecting all access to anything for someone who is not logged in isn’t a useful thing to do. Authorization decides whether to allow access based on who is logged in, with “nobody is logged in” being a perfectly valid option that needs to be handled.

I get what you’re saying and I’m not really arguing against any of those points. After additional digging, I’m thinking my problem is laying elsewhere. Even trying to access pages like the login page:

$this->Authentication->allowUnauthenticated(['login']);

And the Authorization should be returning true, so I should be able to access this page, but I"m still getting an Forbidden exception. I’m getting that on everything actually, unless I’m logged in. I’m trying to figure out where the problem is. I’ll post an update if I find it.

The RequestAuthorizationMiddleware.php is what’s throwing the ForbiddenException.

$result = $service->canResult($identity, $this->getConfig('method'), $request);
if (!$result->getStatus()) {
    throw new ForbiddenException($result);
}

So I’m not sure how to work around this.