mongoid_ability 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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