mongoid_ability 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +22 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +154 -0
- data/Rakefile +9 -0
- data/lib/mongoid_ability.rb +17 -0
- data/lib/mongoid_ability/ability.rb +29 -0
- data/lib/mongoid_ability/ability_resolver.rb +87 -0
- data/lib/mongoid_ability/lock.rb +64 -0
- data/lib/mongoid_ability/owner.rb +59 -0
- data/lib/mongoid_ability/subject.rb +80 -0
- data/lib/mongoid_ability/version.rb +3 -0
- data/mongoid_ability.gemspec +31 -0
- data/test/mongoid_ability/ability_resolver_test.rb +208 -0
- data/test/mongoid_ability/ability_test.rb +91 -0
- data/test/mongoid_ability/lock_test.rb +57 -0
- data/test/mongoid_ability/owner_test.rb +53 -0
- data/test/mongoid_ability/subject_test.rb +102 -0
- data/test/test_helper.rb +122 -0
- metadata +200 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1c92cfc98a8f67825aeaad355026c5f72ae43663
|
4
|
+
data.tar.gz: e571fe34334634fba9f92751354d775af59e4439
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 80315b4061b1a1700d2a5ac4980972203afe88060731315954ef6ec0e6b2bdd6a7094d8f7f2f32bc53385fbb01e7ee29d923d8da10dc562fb39b4b54b748de01
|
7
|
+
data.tar.gz: dd66e44b4e6e513be6e5d128ca8a50457f103071abc759972cbf04f849d5ecabffc875e4fa082455fdd7c60682cb336e0634c37cc99f760bb219eb2104f023af
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Tomas Celizna
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# Mongoid Ability
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/tomasc/mongoid_ability.svg)](https://travis-ci.org/tomasc/mongoid_ability) [![Gem Version](https://badge.fury.io/rb/mongoid_ability.svg)](http://badge.fury.io/rb/mongoid_ability) [![Coverage Status](https://img.shields.io/coveralls/tomasc/mongoid_ability.svg)](https://coveralls.io/r/tomasc/mongoid_ability)
|
4
|
+
|
5
|
+
Custom `Ability` class that allows [CanCanCan](https://github.com/CanCanCommunity/cancancan) authorization library store permissions in [MongoDB](http://www.mongodb.org) via the [Mongoid](https://github.com/mongoid/mongoid) gem.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'mongoid_ability'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
```
|
18
|
+
$ bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
```
|
24
|
+
$ gem install mongoid_ability
|
25
|
+
```
|
26
|
+
|
27
|
+
## Setup
|
28
|
+
|
29
|
+
The permissions are defined by a `Lock` that applies to a `Subject` and defines access for its owner – `User` and/or its `Role`.
|
30
|
+
|
31
|
+
### Lock
|
32
|
+
|
33
|
+
A `Lock` class can be any class that include `MongoidAbility::Lock`.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class MyLock
|
37
|
+
include Mongoid::Document
|
38
|
+
include MongoidAbility::Lock
|
39
|
+
|
40
|
+
embedded_in :owner, polymorphic: true
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
This class defines a permission itself using the following fields:
|
45
|
+
|
46
|
+
`:subject_type, type: String`
|
47
|
+
`:subject_id, type: Moped::BSON::ObjectId`
|
48
|
+
`:action, type: Symbol, default: :read`
|
49
|
+
`:outcome, type: Boolean, default: false`
|
50
|
+
|
51
|
+
These fields define what subject (respectively subject type, when referring to a class) the lock applies to, which action it is defined for (for example `:read`), and whether the outcome is positive or negative.
|
52
|
+
|
53
|
+
For more specific behavior, it is possible to override the `#calculated_outcome` method (should, for example, the permission depend on some additional factors).
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
def calculated_outcome
|
57
|
+
# custom behaviour
|
58
|
+
# returns true/false
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
If you wish to check the state of a lock directly, please use the convenience methods `#open?` and `#closed?`. These take into account the `#calculated_outcome`. Using the `:outcome` field directly is discouraged.
|
63
|
+
|
64
|
+
The lock class can be further subclassed in order to customise its behavior, for example per action.
|
65
|
+
|
66
|
+
### Subject
|
67
|
+
|
68
|
+
All subjects (classes which permissions you want to control) have to include the `MongoidAbility::Subject` module.
|
69
|
+
|
70
|
+
Each action and its default outcome, needs to be defined using the `.default_lock` macro.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
class MySubject
|
74
|
+
include Mongoid::Document
|
75
|
+
include MongoidAbility::Subject
|
76
|
+
|
77
|
+
default_lock :read, true
|
78
|
+
default_lock :update, false
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
The subject classes can be subclassed. Subclasses inherit the default locks (unless they override them), the resulting outcome being correctly calculated bottom-up the superclass chain.
|
83
|
+
|
84
|
+
The subject also acquires a convenience `Mongoid::Criteria` named `.accessible_by`. This criteria can be used to query for subject based on the user's ability:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
ability = MongoidAbility::Ability.new(current_user)
|
88
|
+
MySubject.accessible_by(ability, :read)
|
89
|
+
```
|
90
|
+
|
91
|
+
### Owner
|
92
|
+
|
93
|
+
This gem supports two levels of ownership of a lock: a `User` and its many `Role`s. The locks can be either embedded (via `.embeds_many`) or associated (via `.has_many`). Please make sure to include the `as: :owner` option.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class MyUser
|
97
|
+
include Mongoid::Document
|
98
|
+
include MongoidAbility::Owner
|
99
|
+
|
100
|
+
embeds_many :locks, class_name: 'MyLock', as: :owner
|
101
|
+
has_and_belongs_to_many :roles, class_name: 'MyRole'
|
102
|
+
|
103
|
+
# override if your relation is named differently
|
104
|
+
def self.roles_relation_name
|
105
|
+
:roles
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
class MyRole
|
112
|
+
include Mongoid::Document
|
113
|
+
include MongoidAbility::Owner
|
114
|
+
|
115
|
+
embeds_many :locks, class_name: 'MyLock', as: :owner
|
116
|
+
has_and_belongs_to_many :users, class_name: 'MyUser'
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
Both users and roles can be further subclassed.
|
121
|
+
|
122
|
+
The owner also gains the `#can?` and `#cannot?` methods, that are delegate to the user's ability. It is then easy to perform permission checks per user:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
current_user.can?(:read, resource)
|
126
|
+
other_user.can?(:read, resource)
|
127
|
+
```
|
128
|
+
|
129
|
+
### CanCanCan
|
130
|
+
|
131
|
+
The default `:current_ability` defined by [CanCanCan](https://github.com/CanCanCommunity/cancancan) will be automatically overriden by the `Ability` class provided by this gem.
|
132
|
+
|
133
|
+
## Usage
|
134
|
+
|
135
|
+
1. Setup subject classes and their default locks.
|
136
|
+
2. Define permissions using lock objects embedded (or associated to) either in user or role.
|
137
|
+
3. Use standard [CanCanCan](https://github.com/CanCanCommunity/cancancan) helpers (`authorize!`, `can?`, `cannot?`) to authorize the current user.
|
138
|
+
|
139
|
+
## How it works?
|
140
|
+
|
141
|
+
The ability class in this gem looks up and calculates the outcome in the following order:
|
142
|
+
|
143
|
+
1. User locks, defined for `:subject_id`, then `:subject_type` (then its superclasses), then defined in the subject class itself (via the `.default_lock` macro) and its superclasses.
|
144
|
+
2. Role locks have the same look up chain as the user locks. The role permissions are optimistic, meaning that in case a user has multiple roles, and the roles have locks with conflicting outcomes, the ability favors the positive one.
|
145
|
+
|
146
|
+
See the test suite for more details.
|
147
|
+
|
148
|
+
## Contributing
|
149
|
+
|
150
|
+
1. Fork it ( https://github.com/tomasc/mongoid_ability/fork )
|
151
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
152
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
153
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
154
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "mongoid_ability/version"
|
2
|
+
|
3
|
+
require "mongoid_ability/ability"
|
4
|
+
require "mongoid_ability/ability_resolver"
|
5
|
+
require "mongoid_ability/lock"
|
6
|
+
require "mongoid_ability/owner"
|
7
|
+
require "mongoid_ability/subject"
|
8
|
+
|
9
|
+
# ---------------------------------------------------------------------
|
10
|
+
|
11
|
+
if defined?(Rails)
|
12
|
+
class ActionController::Base
|
13
|
+
def current_ability
|
14
|
+
@current_ability ||= MongoidAbility::Ability.new(current_user)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'cancancan'
|
2
|
+
|
3
|
+
module MongoidAbility
|
4
|
+
class Ability
|
5
|
+
|
6
|
+
include CanCan::Ability
|
7
|
+
|
8
|
+
# ---------------------------------------------------------------------
|
9
|
+
|
10
|
+
attr_reader :user
|
11
|
+
|
12
|
+
# =====================================================================
|
13
|
+
|
14
|
+
def initialize user
|
15
|
+
@user = user
|
16
|
+
|
17
|
+
can do |action, subject_type, subject|
|
18
|
+
subject_class = subject_type.to_s.constantize
|
19
|
+
outcome = nil
|
20
|
+
subject_class.self_and_ancestors_with_default_locks.each do |cls|
|
21
|
+
outcome = AbilityResolver.new(user, action, cls.to_s, subject).outcome
|
22
|
+
break unless outcome.nil?
|
23
|
+
end
|
24
|
+
outcome
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module MongoidAbility
|
2
|
+
class AbilityResolver
|
3
|
+
|
4
|
+
def initialize user, action, subject_type, subject=nil
|
5
|
+
@subject_class = subject_type.to_s.constantize
|
6
|
+
|
7
|
+
raise StandardError, "#{subject_type} class does not have default locks" unless @subject_class.respond_to?(:default_locks)
|
8
|
+
raise StandardError, "#{subject_type} class does not have default lock for :#{action} action" unless @subject_class.self_and_ancestors_with_default_locks.any? do |cls|
|
9
|
+
cls.default_locks.any?{ |l| l.action == action }
|
10
|
+
end
|
11
|
+
|
12
|
+
@user = user
|
13
|
+
@action = action.to_sym
|
14
|
+
@subject_type = subject_type.to_s
|
15
|
+
@subject = subject
|
16
|
+
@subject_id = subject.id if subject.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
# ---------------------------------------------------------------------
|
20
|
+
|
21
|
+
def outcome
|
22
|
+
ua = user_outcome
|
23
|
+
return ua unless ua.nil?
|
24
|
+
|
25
|
+
ra = roles_outcome
|
26
|
+
return ra unless ra.nil?
|
27
|
+
|
28
|
+
class_outcome
|
29
|
+
end
|
30
|
+
|
31
|
+
# ---------------------------------------------------------------------
|
32
|
+
|
33
|
+
def user_outcome
|
34
|
+
locks_for_subject_type = @user.locks_relation.for_action(@action).for_subject_type(@subject_type)
|
35
|
+
|
36
|
+
return unless locks_for_subject_type.exists?
|
37
|
+
|
38
|
+
# return outcome if user defines lock for id
|
39
|
+
if @subject.present?
|
40
|
+
id_locks = locks_for_subject_type.id_locks.for_subject_id(@subject_id)
|
41
|
+
return false if id_locks.any?(&:closed?)
|
42
|
+
return true if id_locks.any?(&:open?)
|
43
|
+
end
|
44
|
+
|
45
|
+
# return outcome if user defines lock for subject_type
|
46
|
+
class_locks = locks_for_subject_type.class_locks
|
47
|
+
return false if class_locks.class_locks.any?(&:closed?)
|
48
|
+
return true if class_locks.class_locks.any?(&:open?)
|
49
|
+
end
|
50
|
+
|
51
|
+
# ---------------------------------------------------------------------
|
52
|
+
|
53
|
+
def roles_outcome
|
54
|
+
locks_for_subject_type = @user.roles_relation.collect(&@user.class.locks_relation_name).flatten.select{ |l| l.subject_type == @subject_type && l.action == @action }
|
55
|
+
|
56
|
+
return unless locks_for_subject_type.present?
|
57
|
+
|
58
|
+
# return outcome if any role defines lock for id
|
59
|
+
if @subject.present?
|
60
|
+
id_locks = locks_for_subject_type.select{ |l| l.subject_id == @subject_id }
|
61
|
+
# for same role, prefer closed lock
|
62
|
+
id_locks = id_locks.reject{ |l| l.open? && id_locks.any?{ |ol| ol.closed? && ol.owner == l.owner } }
|
63
|
+
# across multiple roles, prefer open lock
|
64
|
+
return true if id_locks.any?(&:open?)
|
65
|
+
return false if id_locks.any?(&:closed?)
|
66
|
+
end
|
67
|
+
|
68
|
+
# for same role prefer closed lock
|
69
|
+
class_locks = locks_for_subject_type.reject(&:id_lock?)
|
70
|
+
class_locks = class_locks.reject{ |l| l.open? && class_locks.any?{ |ol| ol.closed? && ol.owner == l.owner } }
|
71
|
+
|
72
|
+
# across multiple roles, prefer open lock
|
73
|
+
return true if class_locks.any?(&:open?)
|
74
|
+
return false if class_locks.any?(&:closed?)
|
75
|
+
end
|
76
|
+
|
77
|
+
# ---------------------------------------------------------------------
|
78
|
+
|
79
|
+
def class_outcome
|
80
|
+
class_locks = @subject_class.default_locks.select{ |l| l.action == @action }
|
81
|
+
|
82
|
+
return false if class_locks.any?(&:closed?)
|
83
|
+
return true if class_locks.any?(&:open?)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module MongoidAbility
|
2
|
+
module Lock
|
3
|
+
|
4
|
+
def self.included base
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.class_eval do
|
7
|
+
field :action, type: Symbol, default: :read
|
8
|
+
field :outcome, type: Boolean, default: false
|
9
|
+
|
10
|
+
# ---------------------------------------------------------------------
|
11
|
+
|
12
|
+
belongs_to :subject, polymorphic: true, touch: true
|
13
|
+
|
14
|
+
# ---------------------------------------------------------------------
|
15
|
+
|
16
|
+
validates :action, presence: true, uniqueness: { scope: [ :subject_type, :subject_id, :outcome ] }
|
17
|
+
validates :outcome, presence: true
|
18
|
+
|
19
|
+
# ---------------------------------------------------------------------
|
20
|
+
|
21
|
+
scope :for_action, -> action { where(action: action.to_sym) }
|
22
|
+
|
23
|
+
scope :for_subject_type, -> subject_type { where(subject_type: subject_type.to_s) }
|
24
|
+
scope :for_subject_id, -> subject_id { where(subject_id: subject_id) }
|
25
|
+
scope :for_subject, -> subject { where(subject_type: subject.class.model_name, subject_id: subject.id) }
|
26
|
+
|
27
|
+
scope :class_locks, -> { ne(subject_type: nil).where(subject_id: nil) }
|
28
|
+
scope :id_locks, -> { ne(subject_type: nil, subject_id: nil) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# =====================================================================
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
end
|
36
|
+
|
37
|
+
# =====================================================================
|
38
|
+
|
39
|
+
def calculated_outcome
|
40
|
+
self.outcome
|
41
|
+
end
|
42
|
+
|
43
|
+
# ---------------------------------------------------------------------
|
44
|
+
|
45
|
+
def open?
|
46
|
+
self.calculated_outcome == true
|
47
|
+
end
|
48
|
+
|
49
|
+
def closed?
|
50
|
+
!open?
|
51
|
+
end
|
52
|
+
|
53
|
+
# ---------------------------------------------------------------------
|
54
|
+
|
55
|
+
def class_lock?
|
56
|
+
!id_lock?
|
57
|
+
end
|
58
|
+
|
59
|
+
def id_lock?
|
60
|
+
self.subject_id.present?
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|