How can I implement an Access Control List in my Web MVC application?
Asked Answered
O

3

98

First question

Please, could you explain me how simpliest ACL could be implemented in MVC.

Here is the first approach of using Acl in Controller...

<?php
class MyController extends Controller {

  public function myMethod() {        
    //It is just abstract code
    $acl = new Acl();
    $acl->setController('MyController');
    $acl->setMethod('myMethod');
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...    
  }

}
?>

It is very bad approach, and it's minus is that we have to add Acl piece of code into each controller's method, but we don't need any additional dependencies!

Next approach is to make all controller's methods private and add ACL code into controller's __call method.

<?php
class MyController extends Controller {

  private function myMethod() {
    ...
  }

  public function __call($name, $params) {
    //It is just abstract code
    $acl = new Acl();
    $acl->setController(__CLASS__);
    $acl->setMethod($name);
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...   
  }

}
?>

It is better than previous code, but main minuses are...

  • All controller's methods should be private
  • We have to add ACL code into each controller's __call method.

The next approach is to put Acl code into parent Controller, but we still need to keep all child controller's methods private.

What is the solution? And what is the best practice? Where should I call Acl functions to decide allow or disallow method to be executed.

Second question

Second question is about getting role using Acl. Let's imagine that we have guests, users and user's friends. User have restricted access to viewing his profile that only friends can view it. All guests can't view this user's profile. So, here is the logic..

  • we have to ensure that method being called is profile
  • we have to detect owner of this profile
  • we have to detect is viewer is owner of this profile or no
  • we have to read restriction rules about this profile
  • we have to decide execute or not execute profile method

The main question is about detecting owner of profile. We can detect who is owner of profile only executing model's method $model->getOwner(), but Acl do not have access to model. How can we implement this?

Onega answered 7/8, 2010 at 11:11 Comment(2)
I don't even understand why you would need "Access control lists" for user interactions. Wouldn't you just say something like if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile() (else, display "You do not have access to this user's profile" or something like that? I don't get it.Havre
Probably, because Kirzilla wants to manage all conditions for access on one place - mainly in configuration. So, any change in permissions can be make in Admin instead of changing code.Afflatus
A
188

First part/answer (ACL implementation)

In my humble opinion, the best way to approach this would be to use decorator pattern, Basically, this means that you take your object, and place it inside another object, which will act like a protective shell. This would NOT require you to extend the original class. Here is an example:

class SecureContainer
{

    protected $target = null;
    protected $acl = null;

    public function __construct( $target, $acl )
    {
        $this->target = $target;
        $this->acl = $acl;
    }

    public function __call( $method, $arguments )
    {
        if ( 
             method_exists( $this->target, $method )
          && $this->acl->isAllowed( get_class($this->target), $method )
        ){
            return call_user_func_array( 
                array( $this->target, $method ),
                $arguments
            );
        }
    }

}

And this would be how you use this sort of structure:

// assuming that you have two objects already: $currentUser and $controller
$acl = new AccessControlList( $currentUser );

$controller = new SecureContainer( $controller, $acl );
// you can execute all the methods you had in previous controller 
// only now they will be checked against ACL
$controller->actionIndex();

As you might notice, this solution has several advantages:

  1. containment can be used on any object, not just instances of Controller
  2. check for authorization happens outside the target object, which means that:
    • original object is not responsible for access control, adheres to SRP
    • when you get "permission denied", you are not locked inside a controller, more options
  3. you can inject this secured instance in any other object, it will retain the protection
  4. wrap it & forget it .. you can pretend that it is the original object, it will react the same

But, there are one major issue with this method too - you cannot natively check if secured object implements and interface ( which also applies for looking up existing methods ) or is part of some inheritance chain.

Second part/answer (RBAC for objects)

In this case the main difference you should recognize is that you Domain Objects (in example: Profile) itself contains details about owner. This means, that for you to check, if (and at which level) user has access to it, it will require you to change this line:

$this->acl->isAllowed( get_class($this->target), $method )

Essentially you have two options:

  • Provide the ACL with the object in question. But you have to be careful not to violate Law of Demeter:

    $this->acl->isAllowed( get_class($this->target), $method )
    
  • Request all the relevant details and provide the ACL only with what it needs, which will also make it a bit more unit-testing friendly:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )
    

Couple videos that might help you to come up with your own implementation:

Side notes

You seem to have the quite common ( and completely wrong ) understanding of what Model in MVC is. Model is not a class. If you have class named FooBarModel or something that inherits AbstractModel then you are doing it wrong.

In proper MVC the Model is a layer, which contains a lot of classes. Large part of the classes can be separated in two groups , based on the responsibility:

- Domain Business Logic

(read more: here and here):

Instances from this group of classes deal with computation of values, check for different conditions, implement sales rules and do all the rest what you would call "business logic". They have no clue how data is stored, where it is stored or even if storage exists in first place.

Domain Business object do not depend on database. When you are creating an invoice, it does not matter where data comes from. It can be either from SQL or from a remote REST API, or even screenshot of a MSWord document. The business logic does no change.

- Data Access and Storage

Instances made from this group of classes are sometimes called Data Access Objects. Usually structures that implement Data Mapper pattern ( do not confuse with ORMs of same name .. no relation ). This is where your SQL statements would be (or maybe your DomDocument, because you store it in XML).

Beside the two major parts, there is one more group of instances/classes, that should be mentioned:

- Services

This is where your and 3rd party components come in play. For example, you can think of "authentication" as service, which can be provided by your own, or some external code. Also "mail sender" would be a service, which might knit together some domain object with a PHPMailer or SwiftMailer, or your own mail-sender component.

Another source of services are abstraction on to on domain and data access layers. They are created to simplify the code used by controllers. For example: creating new user account might require to work with several domain objects and mappers. But, by using a service, it will need only one or two lines in the controller.

What you have to remember when making services, is that the whole layer is supposed to be thin. There is no business logic in services. They are only there to juggle domain object, components and mappers.

One of things they all have in common would be that services do not affect the View layer in any direct way, and are autonomous to such an extent, that they can be ( and quit often - are ) used outside the MVC structure itself. Also such self-sustained structures make the migration to a different framework/architecture much easier, because of extremely low coupling between service and the rest of application.

Analisaanalise answered 13/3, 2012 at 13:35 Comment(23)
I just learned more in 5 minutes rereading this, than I have in months. Would you agree with: thin controllers dispatch to services which collect view data? Also, if you ever accept questions directly, please send me a message.Feder
I partially agree. The collection of data from view happens outside MVC triad, when you initialize Request instance (or some analog of it). The controller only extract data from Request instance and passes most of it to proper services (some of it goes to view too). Services perform operations that you commanded them to do. Then, when view is generating the response, it requests data from services, and based on that information, generates the response. Said response can be either HTML made from multiple templates or just a HTTP location header. Depends on the state set by controller.Sashasashay
To use a simplified explanation: controller "writes" to model and view, view "reads" from model. Model layer is the passive structure in all of Web related patterns that have been inspired by MVC.Sashasashay
@Feder , as for asking question directly, you can always message me in twitter. Or were you question kinda "long-form", that cannot be crammed in 140 chars ?Sashasashay
Reads from the model: does that mean some active role for the model? I've never heard that before. I can always send you a link via twitter if that is your preference. As you can see, these responses turn into conversations quickly and I was trying to be respectful of this site and your twitter followers.Feder
No , it means that view has an active role. Views are the part of application that's responsible for presentation logic. They request data (which is why "read" was in quotes) from model layer through services, and generate response. When dealing with web, you sually will ed up with 1:1 relation between controllers and views.Sashasashay
@Feder , btw , you might also find this post relevant to your research.Sashasashay
Thank you that was great. I'm confused about 1:1 controller:view. Are you talking about a separate class or just a separate method? Also, I always thought controllers called the view. Controller->Model Layer->Controller->View. It seems like it's more like: Controller->Model Layer->View. I always wondered why people talk about thin controllers. Maybe this is why that confused me.Feder
Views are classes, that are responsible for presentation logic, and each view juggles several templates. Controller does not call the current view. It only changes view's state.Sashasashay
let us continue this discussion in chatFeder
Hm, good bit, @tereško, but reading this I suspect the code just after the Demeter should be $this->acl->isAllowed( $this->target, $method ), not $this->acl->isAllowed( get_class($this->target), $method ), true?Interlocutor
I don't think either case would violate LoD by itself. The reason why get_class() is called there is because passing an object to isAllowed() would pointlessly complicate it (especially when writing tests). It does not need the instance, just the class name string. BUT, since there is implementation shown for ACL and __call is just simple example, you are free to write you own implementation. Just keep in mind one thing: $this->target should not be aware of what it is permitted to do. You should not pass it along just to later extract some permissions.Sashasashay
@Analisaanalise "There is no business logic in services. They are only there to juggle domain object, components and mappers." Isn't this juggle still business logic?Kwon
@danip , yes. What services does is called "application logic", according to much smarter people then me :] .. basically, model layer contains three major aspects: application logic, domain logic and data logic.Sashasashay
@Analisaanalise - your clarification really helps. Do you remember and good book where this concept is explained like you present it?Kwon
@danip , most of this comes from Martin Fowler. I would recommend for you to start with reading "Patterns of Enterprise Application Architecture".Sashasashay
@Analisaanalise - can you instantiate domain objects in services? how do you unit test a service?Kwon
@danip Technically, you can. But better way is to pass a factory for instantiating domain objects to a service as a dependency. Makes of easier testing, looser coupling and simpler when it comes to modification. For example, if you are using factory for domain objects, then at some point you can start wrapping every domain object in a variation on that SecureContainer that's described above. Or use similar structure for logging.Sashasashay
@Analisaanalise so all domain objects should be created by factories? Is factory a good name for an object that just creates another object(by that I mean a factory is also a design pattern)Kwon
Yes, it is a pattern, which is why I call it that way.Sashasashay
Since controllers and views typically have a 1:1 relationship, would you add the same decorator to your views when creating them? I guess I'm confused how you would let the user know they don't have access to something with this implementation.Pleione
To be honest, I am not entirely sure (I wrote this thing 3 years ago) why I have an example with a controller there. These days I would be wrapping services in a decorator instead. The original example was meant to receive and AccessDenied exception and change the value, which is used as class name for initializing the classes of views and controller and/or change the value which is use for calling the method (basically, a pseudo-redirect).Sashasashay
What you have to remember when making services, is that the whole layer is supposed to be thin. There is no business logic in services. They are only there to juggle domain object, components and mappers. i like thisCourser
E
16

ACL and Controllers

First of all: These are different things / layers most often. As you criticize the exemplary controller code, it puts both together - most obviously too tight.

tereško already outlined a way how you could decouple this more with the decorator pattern.

I'd go one step back first to look for the original problem you're facing and discuss that a bit then.

On the one hand you want to have controllers that just do the work they're commanded to (command or action, let's call it command).

On the other hand you want to be able to put ACL in your application. The field of work of these ACLs should be - if I understood your question right - to control access to certain commands of your applications.

This kind of access control therefore needs something else that brings these two together. Based on the context in which a command is executed in, ACL kicks in and decisions need to be done whether or not a specific command can be executed by a specific subject (e.g. the user).

Let's summarize to this point what we have:

  • Command
  • ACL
  • User

The ACL component is central here: It needs to know at least something about the command (to identify the command to be precise) and it needs to be able to identify the user. Users are normally easily identified by a unique ID. But often in webapplications there are users that are not identified at all, often called guest, anonymous, everybody etc.. For this example we assume that the ACL can consume a user object and encapsulate these details away. The user object is bound to the application request object and the ACL can consume it.

What about identifying a command? Your interpretation of the MVC pattern suggests that a command is compound of a classname and a method name. If we look more closely there are even arguments (parameters) for a command. So it's valid to ask what exactly identifies a command? The classname, the methodname, the number or names of arguments, even the data inside any of the arguments or a mixture of all this?

Depending on which level of detail you need to identify a command in your ACL'ing, this can vary a lot. For the example let's keep it simply and specify that a command is identified by the classname and method name.

So the context of how these three parts (ACL, Command and User) are belonging to each other is now more clear.

We could say, with an imaginary ACL compontent we can already do the following:

$acl->commandAllowedForUser($command, $user);

Just see what is happeninig here: By making both the command and the user identifiable, the ACL can do it's work. The job of the ACL is unrelated to the work of both the user object and the concrete command.

There is only one part missing, this can't live in the air. And it doesn't. So you need to locate the place where the access control needs to kick in. Let's take a look what happens in a standard webapplication:

User -> Browser -> Request (HTTP)
   -> Request (Command) -> Action (Command) -> Response (Command) 
   -> Response(HTTP) -> Browser -> User

To locate that place, we know it must be before the concrete command is executed, so we can reduce that list and only need to look into the following (potential) places:

User -> Browser -> Request (HTTP)
   -> Request (Command)

At some point in your application you know that a specific user has requested to perform a concrete command. You already do some sort of ACL'ing here: If a user requests a command which does not exists, you don't allow that command to execute. So where-ever that happens in your application might be a good place to add the "real" ACL checks:

The command has been located and we can create the identification of it so the ACL can deal with it. In case the command is not allowed for a user, the command will not be executed (action). Maybe a CommandNotAllowedResponse instead of the CommandNotFoundResponse for the case a request could not be resolved onto a concrete command.

The place where the mapping of a concrete HTTPRequest is mapped onto a command is often called Routing. As the Routing already has the job to locate a command, why not extend it to check if the command is actually allowed per ACL? E.g. by extending the Router to a ACL aware router: RouterACL. If your router does not yet know the User, then the Router is not the right place, because for the ACL'ing to work not only the command but also the user must be identified. So this place can vary, but I'm sure you can easily locate the place you need to extend, because it's the place that fullfills the user and command requirement:

User -> Browser -> Request (HTTP)
   -> Request (Command)

User is available since the beginning, Command first with Request(Command).

So instead of putting your ACL checks inside each command's concrete implementation, you place it before it. You don't need any heavy patterns, magic or whatever, the ACL does it's job, the user does it's job and especially the command does it's job: Just the command, nothing else. The command has no interest to know whether or not roles apply to it, if it's guarded somewhere or not.

So just keep things apart that don't belong to each other. Use a slightly rewording of the Single Responsibility Principle (SRP): There should be only one reason to change a command - because the command has changed. Not because you now introduce ACL'ing in your application. Not because you switch the User object. Not because you migrate from an HTTP/HTML interface to a SOAP or command-line interface.

The ACL in your case controls the access to a command, not the command itself.

Enterprising answered 13/3, 2012 at 16:36 Comment(2)
Two questions: CommandNotFoundResponse & CommandNotAllowedResponse: would you pass these from the ACL class to the Router or Controller and expect a universal response? 2: If you wanted to include method + attributes, how would you handle that?Feder
1: Response is response, here it is not from ACL but from the router, ACL helps the router to find out the response type (not found, especially: forbidden). 2: Depends. If you mean attributes as parameters from actions, and you need ACL'ing with parameters, put them under ACL.Enterprising
S
13

One possibility is to wrap all your controllers in another class that extends Controller and have it delegate all the function calls to the wrapped instance after checking for authorization.

You could also do it more upstream, in the dispatcher (if your application does indeed have one) and lookup the permissions based on the URLs, instead of control methods.

edit: Whether you need to access a database, a LDAP server, etc. is orthogonal to the question. My point was that you could implement an authorization based on URLs instead of controller methods. These is more robust because you typically won't be changing your URLs (URLs area kind of public interface), but you might as well change the implementations of your controllers.

Typically, you have one or several configuration files where you map specific URL patterns to specific authentication methods and authorization directives. The dispatcher, before dispatching the request to the controllers, determines if the user is authorized and aborts the dispatching if he's not.

Servo answered 7/8, 2010 at 11:19 Comment(3)
Please, could you update your answer and add more details about Dispatcher. I have dispatcher - it detects what controller's method I should call by URL. But I can't understand how can I get role (I need to access DB to do it) in Dispatcher. Hope to hear you soon.Onega
Aha, got your idea. I should decide allow executing or not without accessing to method! Thumbs up! The last unresolved question - how to access model from Acl. Any ideas?Onega
@Onega I have the same issues with Controllers. It seems like dependencies have to be in there somewhere. Even if the ACL isn't, what about the model layer? How can you prevent that from being a dependency?Feder

© 2022 - 2024 — McMap. All rights reserved.