mongoid_ability 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](https://travis-ci.org/tomasc/mongoid_ability) [](http://badge.fury.io/rb/mongoid_ability) [](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
|