Permission System - D4uS1/ez-on-rails GitHub Wiki

Permission System

Ez-on-rails provides a permission system to restrict the access to pages and resources. This permission system is group based and configurable in the user management of the administration area. We recommend to save the basic restrictions to the seeds file. Some ez-on-rails generators like ezscaff or ezapi generate default entries to restrict the access. This generators also provide specs to test the restrictions.

There are four types of restrictions we have to distinguish. The restriction to pages the user can call, the restriction to resources the user can own, the restriction of resources the user can be assigned through groups and the restriction of pages protected by api keys. Ez-on-rails does not only provide you with the ability to restrict the acces to actions, controllers and namespaces, it also provides the ability to define groups that can be assigned to users and resources or to define resources as "user owned resources". Those ownerships are restricted to the user who created the resource, but can also be shared to other users or groups.

In the following section you can read some details about the groups. This is recommended because there are some special groups you should know about.

In the [page restrictions section](## Page restrictions) you can read how you can restrict the access to actions, controllers or namespaces.

In the [resource groups section](## Resource groups) you can read about the possibility to assign groups to users and resources, to define some roles on that resources like "manager of ..." or something else.

In the [ownerships section](## Ownerships) you can read about the possibility to define the user owned resource and how to share them.

In the [api keys section](## API Keys) you can read about the possibility to restrict access to namespaces, controllers or actions using api keys.

In the [checking for access section](## Checking for access) you can read how you can check the access to pages and resources with the system. Howver this should be only necessary if you implement some custom action. The resource controllers of ez-on-rails take care about this automaticly.

Groups and group assignments

You can create as many groups as you want. Every group can be connected to a permission that restricts the access to some page or resource. Every user can have multiple groups assigned.

Groups are defined as EzOnRails::Group active record objects. The only thing you need to create a group is the name attribute.

Groups are assigned to users over a has_many through association. The mapping records for this n to m relation are saved using EzOnRails::UserGroupAssignment records. You can access the groups of a user by accessing his groups attribute. This is just a has_many relation targeting the groups. On the other side you can access the users one group has assigned by using its users attribute. This is a has_many relation, too.

There are four types of special groups you need to know about:

The member group

Every user who is registered in your application is automaticly assigned to the member group. This makes it easy to restrict the access to people who are signed in. If you just want to restrict the public access to some resource, you can make use of the member group.

You can not modify the member group and you can not unassign the member group from users.

User groups

Every user who is registered in your application has a user group assigned. This group is named exactly like the user's email. The name is automaticly updated is the user email changes.

The user group is the personal group of the user. Hence, the ez-on-rails permission system does not need to distinguish between users or groups of users. This makes it easy to restrict the access to single users rather than a group of users.

You can not modify the user group and you can not unassign the user group.

The group is meant to be a user group, if the user is assigned using the user_id attribute of the group record.

The api_key group

This group is used to restrict the access to namesapces, controllers or actions by some api key.

You can not modify the api_key grpup.

The super_admin group

Administrators that should have access to everything (including the management of the permission system itself) can be assigned to the super_admin group.

Use this carefully! EzOnRails provides a SuperAdministrator user by default and this should be the only existing Super Admin. But if you need another one for some reason, you can do it, but you should not. I recommend to create a new Administrator group and assign those the users instead.

You can not modify the super_admin group and you can not unassign the super_admin group from the special SuperAdministrator user that should always exists as backup to have an overall admin.

Page restrictions

To restrict the access to pages, you need to create EzOnRails::GroupAccess records. This model holds the attributes action, controller and namespace and is assigned to a group using a belongs_to group association.

As you can see, you can restrict the access to a single action, to all actions of a controller or to all controllers in a namespace. If you define a record having one of this restriction, the access to a user requesting the page will be checked.

Note, if your action does not belong to any restriction, the action is public. If the action is public, everyone can access, even if he is not signed in.

The following diagram describes how the access to the page will be checked:

If the user requests a page, the system checks whether some restriction to the action exists. If there exists an action restriction, the access to the action will be checked. This is done by looking up the users groups. If the user is in the same group like the EzOnRails::GroupAccess record that describes the restriction, the access will be granted. If he has no access, meaning he does not have the group assigned, the access will be denied.

If there does not exist some action restriction, the system will search for some controller restriction. If it exists, the system will lookup the groups of the user like it does for the actions. If the user has access, it is granted, if he does not have access, it is denied.

If there does not exist some action or controller restriction, the system will lookup for the namespace the controller is located in (if exists). If there exists a restriction to the namespace, the user access will be checked. If the user has access, the access is granted. Otherwise it is recjected. Note that the system will handle every nested namespace, too. E.g. If the controller is in the namespace namespace_one/namespace_two, the access will be checked for namespace_one/namespace_two and namespace_one. Every sub-namespace will be checked bottom up.

If there is no of this restrictions defined, the page will be considered as being public and the user gets access.

Here is an example how you can restrict the access to a controller. To restrict the access to actions or namespace, you just have to pass the corresponding values:

# create the group that can be assigned to any action, controller, namespace and user
group = EzOnRails::Group.create name: 'Articles Restriction Group'

# create the restriction to the controller
EzOnRails::GroupAccess.create group: group, namespace: 'content', controller: 'articles', action: nil

# assign the group to the user, hence the user can access it
user = User.find(1337)
user.groups << group

Page restriction using Api Keys

You can restrict the access to namespaces, controllers and actions the same way like with groups.

You only need to create an EzOnRails::GroupAccess record that targets the special group api_key.

The following example shows the restrction for a controller. Like with groups, you can also restrict the access for namespaces and actions.

# create the restriction to the controller
EzOnRails::GroupAccess.create group: EzOnRails::Group.api_key_group, namespace: 'content', controller: 'articles', action: nil

You also can mix this with the default restrictions. For instance, you can restrict the access for some action to an api key, while you have restricted the controller to default user groups, vice versa.

At this point the access to the namespace / controller / action is only allowed if a valid api key is passed via the header 'ai-key'.

You can manage the api keys in the Admin backend, but also can create them programatically.

api_key_record = EzOnRails::ApiKey.create(expires_at: 1.month.since, api_key: '13377331')

This will create a record having the api key '13377331' that expires in 1 month. If you pass the http request header 'api-key': '13377331' you will have access to the restrictee action.

Note that you do not need to pass the api_key value to the creation. The system will generate a secure api_key if you do not pass one. It is saved in the records api_key attribute.

Resource groups

Groups can be assigned to users and resources to make it possible to define "roles" on resources. You can define something like "manager of resource can write the resource".

If you want to use thise, you must create an EzOnRails::OwnershipInfo record and set its resource_groups property to true. If you do so, the ez-on-rails system will now check for the abilities of the user by searching for the groups he is assigned to the resource he wants to access.

EzOnRails::OwnershipInfo.find_or_create_by! resource: Article.to_s do |ownership_info|
  ownership_info.resource_groups = true
  ownership_info.resource =  Article.to_s
end

The groups that should be "resource groups" must have the flag resource_group set to true. There are also three flags you can define on the group to provide the user default functionality to read, write or destroy a resource. Those flags are resource_read, resource_write and __resource_destroy.

EzOnRails::Group.find_or_create_by! name: 'author' do |group|
  group.name = 'author'
  group.resource_group = true
  group.resource_read = true
  group.resource_write = true
  group.resource_destroy = false
end

We recommend to define the ownership infos and groups in the seeds file.

If you now assign a group to a user and an article, he is able to read and write the article, but not to destroy it.

user = User.first
article = Article.first
group = EzOnRails::Group.find_by(name: 'author')

EzOnRails::UserGroupAssignment.create(user: user, group: group, resource: article)

With this system you are able to define roles for resources.

Note that if you change the resource_group flag in the group or the resource_groups flag in the ownership info, the assginments to resources are not removed. If you want to do so, you need to do it yourself. This is for security purposes, because if someone changes one of the flag accidently, you can revert this change and the assignments are available again.

Note: If ou define an ownership info record to use resource groups, all accesses to the resource are denied until you define the groups and its assignments

Also Note: If you define a resource that uses those resource group to be user owned like described in the following section, the user owning system will be evaluated first. Hence if the user has access to the resource eg. because no owner is defined and resources without owner are mentioned as public, he will get access to manage the resource without restriction. In this case setting the flags in the resource groups will have no effect. Hence we recommend either not to define both on resources, or if you do so, you need to get sure that you always define an owner. Normally ez-on-rails does this automaticly for the default actions, but you need to take care about this if you write your own actions.

Normally the necessray associations to check the users ability on resources are created during the ezscaff generator. However if you created the resource manually and want to use the system, you need to add the following lines to the model:

  # associations for the resource_groups access system
  has_many :user_group_assignments, as: :resource, class_name: 'EzOnRails::UserGroupAssignment'
  has_many :groups, through: :user_group_assignments, class_name: 'EzOnRails::Group'

Ownerships

It is pssible to define models as "user owned resources". If this is the case, only the user who created a record of this model can view, edit or destroy the record. Ez-on-rails manages the owner assignment automaticly. The only thing you need to do is to define that a model is an "Ownership Model". The model needs to have the "owner" field, that needs to be a belongs_to association to the user. If you created the model using the ezscaff generator, the model will automaticly have the owner field appended.

If the resource has the owner field, the only thing you need to do is to create an EzOnRails::OwnershipInfo record and set its ownerships property to true. We recommend to do this in your application seeds, that usually are located in the db/seeds.rb file.

EzOnRails::OwnershipInfo.find_or_create_by! resource: Article.to_s do |ownership_info|
  ownership_info.ownerships = true
  ownership_info.resource =  Article.to_s
  ownership_info.on_owner_destroy = :resource_destroy
end

As you can see, you can define what happens to the record if the owner is destroyed by defining the on_owner_destroy attribute. If the following options are possible:

  • resource_nullify - Nullifies the owner field of the resource.
  • resource_destroy - Destroys the resource by using destroy, hence all rails callbacks are executed
  • desource_delete - Destroys the resource by using delete, hence rails callbacks are not executed

Now every record that is created will automaticly have the owner assigned to the current signed in user. But it is also possible to share the resource to other groups or users. The following section will describe how you can enable sharing and how this works.

Note: If a resource that is defined with ownerships has no owner, it is considered to be public. Hence if you have some public action that creates this records, all records created without a signed in user are accessible by everyone. This is a design choice, because you are able to restrict the actions to prevent this. Hence you can choose between those possibilities.

Sharing of Ownership resources

Ownershiped resources can be shared to other groups or users, if the sharing attribute of the ownership definition is set to true:

EzOnRails::OwnershipInfo.find_or_create_by! resource: Article.to_s do |ownership_info|
  ownership_info.ownerships = true
  ownership_info.resource =  Article.to_s
  ownership_info.sharable = true
  ownership_info.on_owner_destroy = :resource_destroy
end

However, you need to add the associations to the model to the groups the resource is shared to.

class Article < EzOnRails::ApplicationRecord
  ...
  has_many :read_accesses, class_name: 'EzOnRails::ResourceReadAccess', dependent: :destroy, as: :resource
  has_many :read_accessible_groups,
           through: :read_accesses,
           source: :group,
           class_name: 'EzOnRails::Group'
  has_many :write_accesses, class_name: 'EzOnRails::ResourceWriteAccess', dependent: :destroy, as: :resource
  has_many :write_accessible_groups,
           through: :write_accesses,
           source: :group,
           class_name: 'EzOnRails::Group'
  has_many :destroy_accesses, class_name: 'EzOnRails::ResourceDestroyAccess', dependent: :destroy,  as: :resource
  has_many :destroy_accessible_groups,
           through: :destroy_accesses,
           source: :group,
           class_name: 'EzOnRails::Group'
  ...
end

As you can see, you simply need to add the groups you want to share the record to, to the read_accessible_groups, write_accessible_groups or/and destroy_accessible_groups association, depending on if you want to give the users that are assigned to the group the ability to read, write or destroy the record.

...
author_group = EzOnRails::Group.find_by name: 'authors'
article = Article.create
article.read_accessible_groups << author_group
article.write_accessible_groups << author_group
...

In this example, all users that are assigned to the authors group are now able to write or read the deleted record. If you use the default rest actions in the EzOnRails::ResourceController or EzOnRails::Api::ResourceController, the permissions will be automaticly checked.

Due to the fact that every user has its own user_group, you are also able to share the record only to single users.

...
chief_editor = User.find_by username: 'John Doe'
article = Article.create
article.read_accessible_groups << chief_editor.user_group
article.write_accessible_groups << chief_editor.user_group
article.destroy_accessible_groups << chief_editor.user_group
...

Now the chief editor can also read, write and delete the record.

If you pass the --sharable option to the ezscaff generator the generator will generate the previously mentioned has_many associations for you.

rails generate ez_on_rails:ezscaff Article title:string --sharable

Tipp: Add the has_many relations to the render info, hence you can edit them in the backend.

module ArticlesHelper
  def render_info_article
    {
      read_accessible_groups: {
        label: t(:'ez_on_rails.ownership_info.read_accessible'),
        label_method: :name
      },
      write_accessible_groups: {
        label: t(:'ez_on_rails.ownership_info.write_accessible'),
        label_method: :name
      },
      destroy_accessible_groups: {
        label: t(:'ez_on_rails.ownership_info.destroy_accessible'),
        label_method: :name
      },
      ...
    }
  end
end

Now you can edit the sharings in the [administration backend[(https://github.com/D4uS1/ez-on-rails/wiki/Administration-Area).

API Keys

TODO

Checking for access

To check the access to an action, you can use the access_to_action?(namespace, controlller, action) helper. This helper is defined in the module EzOnRails::UserAccessHelper and returns whether the user has access to the specifeid combination of namespace, controller and action.

If you want to check the access and automaticly redirect to an access denied action if the check fails, you can use the check_access_to_action(namespace, controlller, action) method.

However every action should normally make those checks out of the box. Normally you dont need to make use of this.

Generally you can check if a user is in a group with the method in_group?(group_name, resource = nil) defined on the user. If you do not pass a resource here, the method will consider to check against static groups without resource assignmets. If you pass :any as resource, the method will return true if the user is assigned to any or no resource and the group. Hence you can use the method to check whether the user eg. is author of "any" article.

article = Article.first

if current_user.in_group? 'Author', article
 ...
end

You can add the user to groups using its method add_group(group_name, resource = nil).

article = Article.first
current_user.add_group 'Author', article

If you use the resource groups or ownerships you can also make use of the default (cancancan)[https://github.com/CanCanCommunity/cancancan] methods to check the access. For resultsets you can eg. use:

result = Article.accessible_by(current_ability, :show)

For single records you can eg. use:

article = Article.first
can? :show, article

If you want to make use of the access helper ez-on-rails provides, you can also check the permission using following methods:

access_to_show_resource? article
access_to_edit_resource? article
access_to_destroy_resource? article
access_to_manage_resource? article

However you need this only in custom actions. Ez-on-rails filters accessible records automaticly for all default actions in its resource controllers.

Important Note: Since we use the load_and_authorize_resource method from cancancan to check the access, you need to exclude additional actions of your controller that are not restful. You have to check the permission yourself. This is because if you use the ownerships, cancancan does not know if it should allow read, write or destroy access for your custom action, because the information is not available. Just exclude it and check the permission on your own.


class Api::ArticlesController < EzOnRails::Api::ResourceController
  ...
  load_and_authorize_resource class: Article, except: [:publish]
  
  before_action :check_publish_access, only: [:publish]
  ...
  
  # GET api/articles/:id/publish 
  def publish
    ...
  end

  private

  def check_publish_access
    raise EzOnRails::ForbiddenError unless access_to_edit_resource? Article.find(params[:id])
  end
end