Skip to content
This repository has been archived by the owner on Aug 23, 2022. It is now read-only.

Commit

Permalink
Merge pull request #11 from powerhome/add-ability-consent-api
Browse files Browse the repository at this point in the history
Delegate permission setting to client apps
  • Loading branch information
xjunior authored Apr 22, 2021
2 parents 890ad22 + 715d29b commit df3b52e
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 290 deletions.
137 changes: 72 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,21 @@ Or install it yourself as:

## What is Consent

Consent makes defining permissions easier by providing a clean, concise DSL for authorization so that all abilities do not have to be in your `Ability`
Consent makes defining permissions easier by providing a clean, concise DSL for authorization
so that all abilities do not have to be in your `Ability`
class.

Consent takes application permissions and models them so that permissions are organized and can be defined granularly. It does so using the
following models:
Consent takes application permissions and models them so that permissions are organized and can
be defined granularly. It does so using the following models:

* View: A collection of objects limited by a given condition.
* Action: An action performed on top of the objects limited by the view. For example, one user could only `:view` something, while another could `:manage` it.
* Subject: Holds the scope of the actions.
* Permission: What is given to the user. Combines a subject, an action and
a view.
* Permission: The combination of a subject, an action, and a view (or full-access).

## What Consent Is Not

Consent isn't a tool to enforce permissions -- it is intended to be used with CanCanCan and is only to make permissions more easily readable and definable.
Consent isn't a tool to enforce permissions -- it supports CanCan(Can) for that goal.

## Subject

Expand All @@ -50,7 +50,8 @@ Consent.define Project, 'Our Projects' do
end
```

The scope is the action that's being performed on the subject. It can be anything, but will typically be an ActiveRecord class, a `:symbol`, or a PORO.
The scope is the action that's being performed on the subject. It can be anything, but will
typically be an ActiveRecord class, a `:symbol`, or a PORO.

For instance:

Expand All @@ -62,16 +63,15 @@ end

## Views

Views are the rules that limit the access to actions. For instance,
a user may see a `Project` from his department, but not from others. That rule
could be enforced with a `:department` view, defined like this:
Views are the rules that limit access to actions. For instance, a user may see a `Project`
from his department, but not from others. You can enforce it with a `:department` view,
as in the examples below:

### Hash Conditions

This is probably the most commonly used and is useful, for example,
when the view can be defined using a where condition in an ActiveRecord context.
It follows a match condition and will return all objects that meet the criteria
and is based off a boolean:
Probably the most commonly used. When the view can be defined using a `where` scope in
an ActiveRecord context. It follows a match condition and will return all objects that meet
the criteria:

```ruby
Consent.define Project, 'Projects' do
Expand All @@ -81,22 +81,21 @@ Consent.define Project, 'Projects' do
end
```

Although hash conditions (matching object's attributes) are recommended,
the constraints can be anything you want. Since Consent does not enforce the
rules, those rules are directly given to CanCan. Following [CanCan rules](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practice)
Although hash conditions (matching object's attributes) are recommended, the constraints can
be anything you want. Since Consent does not enforce the rules, those rules are directly given
to CanCan. Following [CanCan rules](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities%3A-Best-Practice)
for defining abilities is recommended.

### Object Conditions

If you're not matching for equal values, then you would need to use an object
condition, which matches data based off a range.
If you're not matching for equal values, then you would need to use an object condition.

If you already have an object and want to check to see whether the user has
permission to view that specific object, you would use object conditions.
If you already have an object and want to check to see whether the user has permission to view
that specific object, you would use object conditions.

If your needs can't be satisfied by hash conditions, it is recommended that a
second condition is given for constraining object instances. For example, if you
want to restrict a view for smaller volume projects:
If your needs can't be satisfied by hash conditions, it is recommended that a second condition
is given for constraining object instances. For example, if you want to restrict a view for smaller
volume projects:

```ruby
Consent.define Project, 'Projects' do
Expand All @@ -111,7 +110,7 @@ end
```

For object conditions, the latter argument will be the referred object, while the
former will be the context given to the [Permission](#permission) (also check
first will be the context given to the [Permission](#permission) (also check
[CanCan integration](#cancan-integration)).

## Action
Expand Down Expand Up @@ -161,73 +160,81 @@ end

## Permission

A permission is what is consented to the user. It is the *permission* to perform
A permission is what is consented to the user. It consentment to perform
an *action* on a limited *view* of the *subject*. It marries the three concepts
to consent an access to the user.

A permission is not specified by the user, it is calculated from a permissions
hash owned by a `User`, or a `Role` on an application.
## CanCan Integration

The permissions hash looks like the following:
Consent provides a CanCan ability (Consent::Ability) to integrate your
permissions with frameworks like Rails. To use it with Rails check out the
example at [Ability for Other Users](https://github.com/CanCanCommunity/cancancan/wiki/Ability-for-Other-Users)
on CanCanCan's wiki.

In the ability you define the scope of the permissions. This is typically a
user:

```ruby
{
project: {
read: 'department',
approve: 'small_volumes'
}
}
Consent::Ability.new(user)
```

In other words:
You'd more commonly define a subclass of `Consent::Ability`, and consent access
to the user by calling `consent`:

```ruby
{
<subject>: {
<action>: <view>
}
}
class MyAbility < Consent::Ability
def initialize(user)
super user

consent :read, Project, :department
end
end
```

### Full Access
You can also consent full access by not specifying the view:

Full (unrestricted by views) access is granted when view is `'1'`, `true` or
`'true'`. For instance:
```ruby
consent :read, Project
```

In other words:
If you have a somehow manageable permission, you can consent them in batch in your ability:

```ruby
{
projects: {
approve: true
}
}
class MyAbility < Consent::Ability
def initialize(user)
super user

user.permissions.each do |permission|
consent permission.action, permission.subject, permission.view
end
end
end
```

## CanCan Integration
Consenting the same permission multiple times is handled as a Union by CanCanCan:

Consent provides a CanCan ability (Consent::Ability) to integrate your
permissions with frameworks like Rails. To use it with Rails check out the
example at [Ability for Other Users](https://github.com/CanCanCommunity/cancancan/wiki/Ability-for-Other-Users)
on CanCanCan's wiki.
```ruby
class MyAbility < Consent::Ability
def initialize(user)
super user

In the ability you define the scope of the permissions. This is typically an
user:
consent :read, Project, :department
consent :read, Project, :future_projects
end
end

```ruby
Consent::Ability.new(user.permissions, user)
```
user = User.new(department_id: 13)
ability = MyAbility.new(user)

The first parameter given to the ability is the permissions hash, seen at
[Permission](#permission). The following parameters are the permission context.
These parameters are given directly to the condition blocks defined by the views
in the exact same order, so it's up to you to define what your context is.
Project.accessible_by(ability, :read).to_sql
=> SELECT * FROM projects WHERE ((department_id = 13) OR (starts_at > '2021-04-06'))
```

## Rails Integration

Consent is integrated into Rails with `Consent::Railtie`. To define where
your permission files will be, use `config.consent.path`. This defaults to
`app/permissions/` to conform to Rails' standards.
`#{Rails.root}/app/permissions/` to conform to Rails' standards.

## Development

Expand Down
1 change: 0 additions & 1 deletion consent.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
end
spec.require_paths = ['lib']

spec.add_development_dependency 'activesupport', '>= 4.1.11'
spec.add_development_dependency 'bundler', '>= 1.17.3'
spec.add_development_dependency 'cancancan', '~> 1.15.0'
spec.add_development_dependency 'rake', '~> 10.0'
Expand Down
3 changes: 0 additions & 3 deletions lib/consent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
require 'consent/action'
require 'consent/dsl'
require 'consent/permission'
require 'consent/permissions'
require 'consent/ability' if defined?(CanCan)
require 'consent/railtie' if defined?(Rails)

# Consent makes defining permissions easier by providing a clean,
# concise DSL for authorization so that all abilities do not have
# to be in your `Ability` class.
module Consent
FULL_ACCESS = %w[1 true].freeze

# Default views available to every permission
#
# i.e.:
Expand Down
34 changes: 29 additions & 5 deletions lib/consent/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,35 @@ module Consent
class Ability
include CanCan::Ability

def initialize(permissions, *args)
Consent.permissions(permissions).each do |permission|
conditions = permission.conditions(*args)
ocond = permission.object_conditions(*args)
can permission.action_key, permission.subject_key, conditions, &ocond
def initialize(*args, apply_defaults: true)
@context = *args
apply_defaults! if apply_defaults
end

def consent(permission: nil, subject: nil, action: nil, view: nil)
permission ||= Permission.new(subject, action, view)
return unless permission.valid?

can(
permission.action_key, permission.subject_key,
permission.conditions(*@context),
&permission.object_conditions(*@context)
)
end

private

def apply_defaults!
Consent.subjects.each do |subject|
subject.actions.each do |action|
next unless action.default_view

consent(
subject: subject.key,
action: action.key,
view: action.default_view
)
end
end
end
end
Expand Down
29 changes: 12 additions & 17 deletions lib/consent/permission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,29 @@

module Consent
class Permission # :nodoc:
def initialize(subject, action, view = nil)
@subject = subject
@action = action
@view = view
end
attr_reader :subject_key, :action_key, :view_key, :view

def subject_key
@subject.key
def initialize(subject_key, action_key, view_key = nil)
@subject_key = subject_key
@action_key = action_key
@view_key = view_key
@view = Consent.find_view(subject_key, view_key) if view_key
end

def action_key
@action.key
def action
@action ||= Consent.find_action(subject_key, action_key)
end

# Disables Sytle/SafeNavigation to keep this code
# compatible with ruby < 2.3
# rubocop:disable Style/SafeNavigation
def view_key
@view && @view.key
def valid?
action && (@view_key.nil? == @view.nil?)
end

def conditions(*args)
@view && @view.conditions(*args)
@view.nil? ? nil : @view.conditions(*args)
end

def object_conditions(*args)
@view && @view.object_conditions(*args)
@view.nil? ? nil : @view.object_conditions(*args)
end
# rubocop:enable Style/SafeNavigation
end
end
41 changes: 0 additions & 41 deletions lib/consent/permissions.rb

This file was deleted.

9 changes: 0 additions & 9 deletions lib/consent/subject.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,5 @@ def initialize(key, label)
@actions = []
@views = Consent.default_views.clone
end

def permission_key
ActiveSupport::Inflector.underscore(@key.to_s).to_sym
end

def view_for(action, key)
view = @views.keys & action.view_keys & [key]
@views[view.first] || @views[action.default_view]
end
end
end
Loading

0 comments on commit df3b52e

Please sign in to comment.