4

What are the best practices to extend Authorization to the Model layer if in a monolith MVC application (i.e rails) you have other entry-points than just the Controller? I.e background jobs, or the model interaction?

Regarding security

Normally we enqueue jobs in a queue, to off-load the main thread, then we have a task which try to process the queue, either in the next tick or between fixed interval times. This task normally have to impersonated by a specific user, to avoid to have the wrong access level, when it's CRUD'ing the models.

Regarding audit

Another related issue that I see is regarding Audit. Without passing a user instance or reference to the models, there is no way to fire callbacks based on Model events to log the actions properly, without passing the current_user to the models.

One solution for that is to have the audit generation in the controller, but then again, IMO Audit generator doesn't belong to the Controller (normally it can happens async, not in the main thread), and I would have to repeat the same "Audit generation" in other places too, i.e "Background jobs", leaving the model interaction blindly happen. Is that the Observer pattern the one that I'm searching for?


Disclaimer

I'm not interested in ruby gems to achieve audit, policy checking or authentication, but more in the design pattern to work with Authentication and Authorization when the Controller isn't the only way to interact with the models.

  • 2
    when the Controller isn't the only way to interact with the modelsWhy? Why these other interactions cannot be forwarded to controllers? You can have more controllers besides those specifically dedicated to the MVC. – Laiv Apr 5 at 6:36
  • 2
    I think you need to clarify your question. What do you mean by Model Layer? Do you mean View Models? If so, do you share your View Models with another applications such as Background jobs ? And is there any limitation not to consider service layer which can be called by even controller and background jobs? – Engineert Apr 5 at 6:42
  • @Laiv controllers are there to respond to http requests, and to the middleman between views and models. The example of a background job is a good one. How do you authorize the jobs to run as specific user, without having some Authorization logic in the models – VP. Apr 5 at 8:03
  • 1
    controllers are input boundaries where to put such a control. You can implement the second class of controllers for these jobs. Controllers with a specific interface that meets the needs for security (initialise the Security context, checks tokens or some kind of static credentials, etc) – Laiv Apr 5 at 8:05
  • 1
    On the other hand, why these internal jobs should go through security? These are likely to use self-generated credentials made in-expresso for them. Security makes sense towards external actors of the system. Jobs are interna actors. Aren't they? – Laiv Apr 5 at 11:16
2

So, theory first:

Generally speaking, Authentication and Authorization are cross cutting concerns which should not be part of the Models.

In MVC the controller is the "entry point" for all actions, user generated or not. A background process which is part of the same application should call a controller.

In practice, it sounds like you are trying to cram too much into a single application. I'm going to assume this is a web application going forward. Instead of a single application I would split it as follows

  1. Authentication service

    This performs Authentication and issues tokens with roles

  2. API or APIs

    This exposes your service methods, BuyProduct, RegisterCustomer, CalibrateModel or whatever. It uses token based authentication from the Auth service

  3. WebSite

    This uses the Auth service to allow users to login and passes the generated token to the API layer in order to perform its functions

  4. Background Jobs

    These run on seperate servers and each has its own service user. The application uses that user to login via the auth service and passes the generated token to the API layer to perform its functions

Now each one of these layers will potentially have models views and controllers. some of the models may be shared but you have multiple "entry points" where you can apply logging and authentication checks with user context, outside of the model.

  • yes I forgot to add it's a monolith, 13 years old web application, well tested and up-to-date :) – VP. Apr 5 at 9:07
2

I highly recommend separate your project as layers and have a Service Layer that can be used by your many projects such as MVC and/or Background Jobs and you can add this Authorization Service to Service Layer. So you can call this Authorization Service Layer.

This way, you don't need to extend Authorization to the Model layer because your other entry-points can call this layer. Besides, Models is not responsible about Authentication. It is worse than being on Controllers.

Additionally, there are kind of authorization such as Role-Right Management. These authorizations can be for per your business logic. If so, you should also call required validation method to check this authorization from your business logic layer. By doing this, even you don't check if users authorize(especially from background jobs), they can not perform this action if not authorized.

public class TaskService
{
    private readonly AuthorizationService _authorizationService;

    public TaskService(AuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }

    public void AssingTask(int taskId, int userId)
    {
        // you can perform this validation by attributes that properly generated for your design.
        // It is just sample
        // by this validate there is no user can do this action without having required right(s).
        this.Validate(Rights.AssignTask, userId);

        // do your assign business.
    }

    private void Validate(Rights right, int userId)
    {
        if (!_authorizationService.IsUserAuthorize(right, userId))
            throw new Exception("User has not right for this action.");
    }
}

Next step is better way to use this authorization methods on your monolith MVC layer. Again I highly recommend, create static extension methods and call it throughout the project for authorization. You can call this static extension methods by your User object from view as:

in view:

@if(User.IsAuthorize("Something"))
{
   <submit type = "button" text ="you can do this" />
} 

and controller:

public class TaskController : Controller
{

    [CustomAuthorize(Right = Rights.AssignTask)]
    public ActionResult TaskAssing()
    {
        _taskService.AssignTask(userId);
    }
}

CustomAuthorize attribute:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public class CustomAuthorize : // ...
    {
    public Rights Right { get; set; }

    public void OnAuthorization(AuthorizationContext filterContext)
    {
        if (!AuthorizeExtension.IsAuthorize(filterContext.HttpContext.User.Identity, Right))
        {
            redirect("UnAutorize");
        }

    }
}

static extension method:

public static class AuthorizeExtension
{
    public static bool IsAuthorize(this IPrincipal principal, Rights right)
    {
        return _authorizationService.IsUserAuthorize(right, principal.Identity.Id);
    }
}

Last, what makes users are authorized? It's totally up to you and your design. But, there are two option I can suggest:

1. Role-Right Management

You can define a right for each business function. For example Task Create, Task Assign, Task Delete, Start Task, Stop Task, User Create, User Update, Prepare Report, Report Details ... Should defined all rights for all business functions. Then let role can have multiple rights.((n)Role ->(n)Right). Then, create User-Role relations. It's also (n)User->(n)Role relation. You need to store all these relations to database.

This design, let you manage authorization for all business function by adding rights to role and assigning roles to users. In your IsUserAuthorize method in Authorization Service Layer, you can check if user has required right by his/ her role(s).

2. Minimum Type Of Authorized Users

You can determine minimum type of authorized users and determine what they are able to do. You can define all things as enum and can determine which user is which type by code.

public enum Roles
{
    AdminRole,
    StandartUser,
    NewOne,
    Guest
}

public enum Services
{
    Task,
    Report,
    User
    //...
}

public class AuthorizationService
{
    public bool IsUserAuthorize(int userId, Services service)
    {
        Roles role = DetermineUserRole(userId);

        switch(role)
        {
            case Roles.AdminRole:
                return true;
            case Roles.Guest:
                return service == Services.Report ;
            // ...

            default:
                throw new Exception("Can not determine user role");
        }
    }
}

Which one you should choose?

First option is really good practice and you can manage all functions. Also, you can let users to create new role and assign to others.

But, as you mentioned, your project monolith and probably there will be no major change or add new functionality. Thus, it seems better to choose second one. So, you won't need to define right for all business functions and store those rights and relations to database.

2
+100

if in a monolith MVC application (i.e rails) you have other entry-points than just the Controller? I.e background jobs, or the model interaction?

Consider workers (jobs executors) as actors of the system. Give them an identity in the security service so that they can engage with it like any other actor. Grant them with special privileges if necessary. You say: -- This task normally has to impersonated by a specific user, to avoid to have the wrong access level. Good, you are halfway there.

Make input ports of the application accessible for workers. Inputs ports are the gate through which outer-layers get access to the system. You already have these ports; the controllers. They fit perfectly with the purpose, however, they probably are very specialized in handling web inputs, so it might take you to do some adaptations. Perhaps, with a little luck, a couple of basic proxies for a couple of controllers. Perhaps you have to make accessible some more. Then a gateway should do the job too. Both are good abstractions to engage with security.

public class BatchWorker {
    //.... 
    public void execute(){
       securityService.doLogin(credentials);
       job.run();
    }
}

public class Job {
    //.... 
    public void run(){
       salesController.aSalesMethod(input);
    }
}

public interface SalesController {}

public final class SalesControllerProxy implements SalesController {
   private final SalesController salesController;
   private final SecurityService;
   //...
   public void aSalesMethod(InputData data){  
     if(securityService.userInRoles("role_a")){
        salesController.aSalesMethod(data);
     };

   }
}

Regarding audit, you are right. The audit, the logging, the security are cross-cutting concerns that usually bloat our source code, making the reading of the business a little bit harder. Not to mention coupling. You can mitigate these things (to a point) with Aspect-oriented programming or with proxies|gateways.

  public final class SalesControllerProxy implements SalesController {
     // the previous code ...
     private final Auditor auditor;
     private final Logger logger;

     public void aSalesMethod(InputData data){
        ...
        doLog(data);
        ...
        doAudit(data);
     }

     // rest of the code ...
     public void doLog(InputData data){  
         if(logger ! = null){
            logger.debug("Useful messages for developers go here");
         } 
     }

     public void doAudit(InputData data){
        Principal principal = securityContext.getPrincipal();  
        if(auditor ! = null){
           auditor.trace("Useful messages for monitoring and traceability go here",principal);
         }
     }
  }

Proxy pattern fits in any layer. Even in the model (domain).

Say we declare an interface for Product and 2 implementations: one for the business rules and the proxy for audit. Their respective instantiations are delegated to factories or builders; as prefered. As soon as the business rules use the interface Product we have all the capabilities of the proxy and the entity indistinguishable and decoupled at the same time. That said, if we go down that way, it's advisable avoiding reflection. It's tempting, but it violates a handful of OOP principles and makes the code cumbersome.

With this approach I propose:

  • Small changes: I would expect some refactors alongside with new classes, but not a big redesign. The solution should not make you break the monolith. That should be caused by other issues.
  • Code reuse: There's already working code. Turn it into your stable dependency.
  • Testability: The mentioned patterns, DI and good practices should result in code easy to test.
  • Concern segregation: move audit, logging and security away (as much as possible) from the business. Business should not be affected by these details.
  • Documenting: Patterns speak about problems. Common problems. They are a useful tool for communication between developers.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.