go-goyave / goyave

🍐 The enterprise REST API framework
https://goyave.dev
MIT License
1.57k stars 66 forks source link

Permission system (guards) #43

Open System-Glitch opened 4 years ago

System-Glitch commented 4 years ago

Proposal

Guards are an extension of the authentication system using user-defined fields from the authenticator's user model to allow or deny specific actions to an authenticated user.

For example, a Goyave application will define a guard for forums moderation. A moderator is allowed to modify the posts of other users, but regular users aren't. A guard update-others will be implemented, using the IsModerator field from the user model and the route parameter to check if the user is a moderator or if its own post.

Guards could be used in two ways:

This system could support OAuth in some way too.

The implementation is not defined yet, so feel free to discuss and suggest one.

Possible drawbacks

None.

Additional information

This issue is a feature proposal and is meant to be discussed. It is also a good candidate if you want to contribute to the project.

amit-davidson commented 4 years ago

@System-Glitch I have an idea on my mind. I'll write a proposal and we can discuss it.

amit-davidson commented 4 years ago

Abstract

I propose a permission system based on RBAC. Every user will have a list of roles with specific permissions saved in the DB. The permissions will specify what a role is allowed to do and on what items. Anytime a validation is required, the validation will compare the permissions of the user passed with the request against the required permissions. The validation will be called from the code for handlers and from a built-in middleware for routes.

Background

RBAC - (Role-based access control) is based on defining a list of roles and adding each user in the system to one or more roles. Permissions and privileges are then granted to each role, and users receive them via their membership in the role.

ACL- (Access-control list) is a list of permissions attached to an object. It specifies which users or system processes are granted access to objects, as well as what operations are allowed on given objects.

RBAC defines permissions from the view of the roles, which means: roleA can read objectA and write to objectB whereas ACL is set from the perspective of the item: objectA allows userA to delete it, and userB to read it.

Proposal

Initialization

A new field named Roles will be added to the user object. I think the easiest way to declare the permissions, for now, is in the DB. The permissions will be fetched alongside the username and password in the authenticate middleware and attached to the user object. In the future, we can add support for loading the permissions from a file or add them from the code.

Permissions Structure

The following JSON depicts the structure of the permissions.

{
  User: {
    Roles: [
      {
        Name: "UserA",
        "Policies": [
          {
            "Name": "PolicyA",
            "Effect": "Allow/Deny",
            "Resource": "ModelA",
            "Permissions": ["A", "B", "C", "D"]
          }
        ]
      }
    ]
  }
}

Few points to notice:

How it will be used

A new function called IsAuthorized will be added to Goyave. Its signature is: IsAuthorized(user interface{}, permissions []string, ownerName string, itemId string) where:

  1. For the endpoints themselves, a list of permissions will be added on the route when it is defined. The check will run as a middleware after authentication. User is given from the request. The permissions are provided, and ownerName and itemID will be left empty as they have no meaning.

Rationale

The reason I propose an RBAC solution instead of ACL is because the developer will have many resources in the system and not many roles. That way, it will be easier to define a small number of roles in relation to many resources instead of defining permissions on many resources.

Feel free to comment if you have different suggestions or something is not clarified. 😃

After we settle on the proposal, I'll break the design into steps, and we'll start working from there.

System-Glitch commented 4 years ago

Great proposal. I do have a few questions and comments though.

I think that the policy should be "Deny" by default, and that only "Allow" policies are needed. However, "Deny" could be useful in a case where the user has multiple roles, one allowing access to a resource and one denying it. In this scenario, which policy has priority?

Roles should be put in a separate model and database table, to avoid redundancy and potential mismatches.

The default User model can be replaced by the developer, how would the framework define a pivot table for the n:n relation between the custom User model and our Role model? Also how should we manage migrations in that case?

User in the request object is an interface{}, so some reflection would be needed to retrieve it as a struct. Then, how would the framework know which struct field to use? A new field tag may be needed, for example: auth:roles.

I like the resource field for URIs very much, but for models, I don't think we can implement that. At the time middleware is executed, we don't know which model or which action the controller handler is going to use. The same goes for permission list.

About IsAuthorized, can we move the permissions list to make it the last argument of the function, and make it variadic?

amit-davidson commented 4 years ago

One last thing, I thought about more use cases for permission checking except using models for accessing the DB or for routes. For example, a user might want to have an option to clear the cache, restart a service, send a notification, etc.). By using resourceType for identifying the resource type, the developers are limited to models and routes. I'll try to think of generalized way resources can be recognized instead of a custom ID for each type. I'll handle this point together with points we decide to change.

System-Glitch commented 4 years ago

Note: I took a lot of inspiration from the PHP Laravel framework for certain parts of Goyave. Laravel provides an Authorization feature which may be a good source of inspiration.

amit-davidson commented 4 years ago
System-Glitch commented 4 years ago

Calling IsAuthorized both in the handler and in the middleware is redundant. It should be possible but shouldn't be the primary way to do it, especially if it's only for models. Laravel does it so we can also use the authorization system in artisan commands, scheduled jobs, etc.

In short: authorization should always be defined in the route definition, preferably as a middleware. However, the developer should still be able to check authorization in another context (with the can function for example), such as in a background go routine, a websocket, etc. Like this, the system is flexible enough to suit everyone's need.

I think custom authorization function is indeed a must, and is probably the best way to setup authorization on models and services that are not related to database records. I think we should focus on developing a base for the system and make it expandable, like the Goyave auth system: you can add more authenticators easily. Then we'll add a basic guard covering the most common authorization cases: list, show, create, update, delete. More complex cases such as the moderator being able to edit other users's post may be implemented by the developer.

amit-davidson commented 4 years ago

I have most of it on my mind, but there are a few more points I'm not confident about yet. If it's possible for you, I think it's better if we sum it up over an audio call like zoom or even chat.

System-Glitch commented 4 years ago

I'll email you to know how we can contact each other. A summary of what's been said will be posted here as well.

System-Glitch commented 4 years ago

Summary of yesterday's chat

Roles and policies will be dynamic and persistent: two tables will be created by the framework.

A Permissionable struct will be added. This struct shall be promoted inside a user model.

type Permissionable struct {
  Roles []Role `gorm:"many2many:user_roles;"`
}

This allows us to take advantage of the relation handling in Gorm.


Roles and policies will be manageable through standard functions such as auth.CreateRole, myRole.AddPolicy(), etc. Therefore, the developer will be able to register roles in different ways:

Most likely use-cases:

This system allows developers to move from use-case to another very easily and would require minimal changes to the application code to extend or shrink its roles functionalities mid-development.


An authorization middleware will be implemented. This middleware will load the user's related roles and check if he has access to the requested route. Once loaded, the roles stay available and can be used inside custom middleware and controller handlers.

Thanks to the Permissionable struct, reflection is not needed, we can use type assertion.


Roles and users won't be loaded at all times into memory for better memory efficiency and scalability, at the cost of very slightly longer response time.


On top of the URI authorization definition, developers will be able to implement custom authorization functions. These functions will be located in a new folder in the recommended directory structure. This directory name is still to be defined.

System-Glitch commented 4 years ago

I am still looking for contributors to implement this feature.

dhairya0904 commented 3 years ago

I am still looking for contributors to implement this feature.

I can help.

System-Glitch commented 3 years ago

Hello @dhairya0904 ! Thank you very much for your help ! Don't hesitate to contact me (via email, telegram or discord) if you have any question or if you need guidance.

Shall I move this issue to "In progress"?

dhairya0904 commented 3 years ago

Hello @dhairya0904 ! Thank you very much for your help ! Don't hesitate to contact me (via email, telegram or discord) if you have any question or if you need guidance.

Shall I move this issue to "In progress"?

Sure