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 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
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ cache: bundler
3
+ script: 'bundle exec rake'
4
+ rvm:
5
+ - 2.1.2
6
+ services:
7
+ - mongodb
8
+
9
+ notifications:
10
+ email:
11
+ recipients:
12
+ - tomas.celizna@gmail.com
13
+ on_failure: change
14
+ on_success: never
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongoid_ability.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :minitest do
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
6
+ watch(%r{^test/.+_test\.rb$})
7
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
8
+ end
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,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task :default => :test
@@ -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