scram 0.1.0
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/MIT-LICENSE +20 -0
- data/README.md +173 -0
- data/Rakefile +32 -0
- data/app/models/scram/policy.rb +67 -0
- data/app/models/scram/target.rb +70 -0
- data/lib/scram.rb +11 -0
- data/lib/scram/concerns/aggregate_holder.rb +23 -0
- data/lib/scram/concerns/holder.rb +38 -0
- data/lib/scram/core_ext/symbol_extensions.rb +14 -0
- data/lib/scram/dsl/builders.rb +43 -0
- data/lib/scram/dsl/definitions.rb +36 -0
- data/lib/scram/dsl/model_conditions.rb +50 -0
- data/lib/scram/engine.rb +15 -0
- data/lib/scram/version.rb +3 -0
- data/lib/tasks/scram_tasks.rake +4 -0
- data/spec/config/mongoid.yml +6 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +5 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +34 -0
- data/spec/dummy/bin/update +29 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +23 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +9 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +51 -0
- data/spec/dummy/config/environments/production.rb +83 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/new_framework_defaults.rb +21 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/mongoid.yml +147 -0
- data/spec/dummy/config/puma.rb +47 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/log/development.log +11 -0
- data/spec/dummy/log/test.log +2321 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/factories/policy.rb +0 -0
- data/spec/rails_helper.rb +78 -0
- data/spec/scram/concerns/aggregate_holder_spec.rb +58 -0
- data/spec/scram/concerns/holder_spec.rb +100 -0
- data/spec/scram/dsl_spec.rb +51 -0
- data/spec/scram/policy_spec.rb +28 -0
- data/spec/scram/target_spec.rb +40 -0
- data/spec/scram/test_implementations/simple_aggregate_holder.rb +21 -0
- data/spec/scram/test_implementations/simple_holder.rb +21 -0
- data/spec/scram/test_implementations/test_model.rb +10 -0
- data/spec/scram_spec.rb +11 -0
- data/spec/spec_helper.rb +99 -0
- data/spec/support/factory_girl.rb +9 -0
- metadata +278 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4d4f42aaff808f60d5d9c3c1fd55749eb3b04f6a
|
4
|
+
data.tar.gz: 04e6595617fdea9551367a3e3cfff2c77cacb8c3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 20c53c83ef0fccbb4ab243a7e29bd950840428bbbbe5795f466b4ae2bef8cab43d7eb7dfde66596c48bae33c4f45df1c2834dc7e77d27ee53327c301d3ac0cdc
|
7
|
+
data.tar.gz: 242ed1189b2f8d7074def6194593c1fead56d0d8090ddfd73a1031aafc6c4512be40b4b9e555cd3e6af8a7085353c1975a415945fff783a5d494d8401dc913e8
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 skreem
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# Scram [](https://travis-ci.org/skreem/scram) [](https://coveralls.io/github/skreem/scram?branch=master)
|
2
|
+
An awesome authorization system
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
|
6
|
+
Add this line to your application's Gemfile:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
gem 'scram'
|
10
|
+
```
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install scram
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
[Click here to see YARD Documentation](http://www.rubydoc.info/github/skreem/scram/master)
|
23
|
+
|
24
|
+
### Quick Overview of Scram
|
25
|
+
- Holder
|
26
|
+
- Scram doesn't force you to use a specific group system. Instead, just include `Holder` into any class which can hold `Policies`.
|
27
|
+
- Scram provides a class for objects like Groups through an `AggregateHolder`. This is a class which should be included in anything which holds policies through other holders.
|
28
|
+
- In most cases, your `AggregateHolder` will be a `User` model. Your `Group` model will be a `Holder`. If you don't want to use a group system, then your `User` model will likely be a `Holder`.
|
29
|
+
- Policy
|
30
|
+
- Policies are used to bundle together permissions.
|
31
|
+
- There are 2 kinds of `Policies`: Those for a specific model, and "global" `Policies` for permissions that aren't bound to a specific model.
|
32
|
+
- Target
|
33
|
+
- `Targets` are a way to declare what actions are allowed in a `Policy`.
|
34
|
+
- `Targets` have a list of actions and conditions.
|
35
|
+
- Actions are anything a user can do to an object. For example: `:update, :view, :create, :destroy`.
|
36
|
+
- Conditions are used to refine what instances a target applies to. They support basic comparisons to attributes, but can be used to support more complex logic with the DSL.
|
37
|
+
|
38
|
+
### Example Usage
|
39
|
+
If you choose to implement Holder into your user class directly, it may look something like the following.
|
40
|
+
```ruby
|
41
|
+
class User
|
42
|
+
...
|
43
|
+
include Scram::Holder
|
44
|
+
|
45
|
+
# Will automatically implement the needed #policies method for Holder.
|
46
|
+
has_many :policies, class: "Scram::Policy"
|
47
|
+
|
48
|
+
def scram_compare_value
|
49
|
+
self.id
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
This sort of system would not include a group system at all (for simplicity). If you want a Group system, have your user include `Scram::AggregateHolder` and then implement `#aggregates` to return your groups (which should be `Holder`s themselves).
|
54
|
+
|
55
|
+
We will be providing a full fledge example application of Scram shortly which will include a Group and membership system, and will clarify how the `AggregateHolder` system works. For now, lets see how Scram works in its simplest usage (a user who stores policies just for themselves).
|
56
|
+
|
57
|
+
#### Adding a String Permission
|
58
|
+
Now lets add a String permission to display a statistics bar for users like admins. We want to call `user.can? :view, "peek_bar"` and have it return true for admins.
|
59
|
+
|
60
|
+
To do this, we'll need to define a non-model Policy (because our object is a string, "peek_bar").
|
61
|
+
```ruby
|
62
|
+
user = ...
|
63
|
+
policy = Scram::Policy.new
|
64
|
+
policy.name = "global-strings-policy" # Note that we're setting name, and we will leave context nil.
|
65
|
+
policy.context = nil # This would be nil by default as well. By not setting this to anything, we let this Policy handle String permissions, and not be bound to a model.
|
66
|
+
policy.save
|
67
|
+
user.policies << policy
|
68
|
+
user.save
|
69
|
+
```
|
70
|
+
|
71
|
+
Now we want to add a target that represents the ability to `view "peek_bar"`.
|
72
|
+
```ruby
|
73
|
+
target = Target.new
|
74
|
+
target.conditions = {:equals => { :'*target_name' => "peek_bar"}}
|
75
|
+
target.actions << "view"
|
76
|
+
policy.targets << target
|
77
|
+
policy.save
|
78
|
+
```
|
79
|
+
|
80
|
+
This code creates a target which permits viewing if the `*target_name` equals "peek_bar".
|
81
|
+
|
82
|
+
Scram automatically replaces `*target_name` with the action being compared. For example, in `can? :view, "something_else"` Scram would check if `"something_else" == "peek_bar"`.
|
83
|
+
|
84
|
+
And now we're done! :tada:
|
85
|
+
|
86
|
+
#### Adding a Model Permission
|
87
|
+
Now lets add something a bit more complex. Say we're developing a Forum application. We want to add the ability for a user to edit their own `Posts` using Scram.
|
88
|
+
|
89
|
+
Here's our simple `Post` model:
|
90
|
+
```ruby
|
91
|
+
class Post
|
92
|
+
...
|
93
|
+
belongs_to :user
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Lets make a Policy that handles post related permissions.
|
98
|
+
```ruby
|
99
|
+
user = ...
|
100
|
+
policy = Scram::Policy.new
|
101
|
+
policy.name = "Post Stuff" # This name is just for organizational/display purposes
|
102
|
+
policy.context = Post.to_s # Note: By setting context, we bind this policy to the model "Post"
|
103
|
+
policy.save
|
104
|
+
user.policies << policy
|
105
|
+
user.save
|
106
|
+
```
|
107
|
+
|
108
|
+
Now we need a Target in our Policy to let users edit their own Posts.
|
109
|
+
```ruby
|
110
|
+
target = Target.new
|
111
|
+
target.conditions = {:equals => {:user => "*holder"}}
|
112
|
+
target.actions << "edit"
|
113
|
+
policy.save
|
114
|
+
```
|
115
|
+
What is `*holder`? Well, Scram replaces this special variable with the current user being compared. In `User#scram_compare_value` we return the User's ObjectId, and this is exactly what Scram replaced `*holder` with.
|
116
|
+
|
117
|
+
So now this Target reads "allow a holder to edit a Post if the user of that Post is the holder". Pretty neat, huh?
|
118
|
+
|
119
|
+
And now we're done! Go ahead and call `holder.can? :edit, @post` on a post which they own, and you'll see that Scram allows it! :tada:
|
120
|
+
|
121
|
+
#### Using conditions
|
122
|
+
In our last example, we let Scram directly compare an attribute of the model. What if we need more complex checking behavior? Luckily, Scram provides a DSL for models to easily define conditions which can be referenced in the database in place of attributes.
|
123
|
+
|
124
|
+
Lets revisit the `Post` example, but this time we'll define how to get the owner using a condition DSL, instead of the attribute.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
class Post
|
128
|
+
include Scram::DSL::ModelConditions
|
129
|
+
...
|
130
|
+
belongs_to :user
|
131
|
+
|
132
|
+
scram_define do
|
133
|
+
condition :owner do |post|
|
134
|
+
post.user
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
Now we no longer need to directly tell our Target to access the user field. Here's what the equivalent Target would look like from our previous example, now using the new condition:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
...
|
144
|
+
target.conditions = {:equals => {:'*owner' => "*holder"}}
|
145
|
+
...
|
146
|
+
```
|
147
|
+
|
148
|
+
Scram is smart enough to realize that any key starting with an `*`, like `*owner`, is a manually defined condition. Now, calling `user.can? :edit, @post` will compare the value returned by the `condition` block to the hash value (which in this case is the Holder).
|
149
|
+
|
150
|
+
#### Defining a New Comparator
|
151
|
+
You may have noticed from the previous examples that the keys of our Target conditions were things like `equals` and `less_than`. These come from our Comparator definitions (see `Scram::DSL::Definitions::COMPARATORS`).
|
152
|
+
|
153
|
+
These comparators are defined using the DSL for comparators. We provide a basic set of comparing operators, but you may need to add your own. To do this, we recommend creating an initializer file and then calling something like the following:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
builder = Scram::DSL::Builders::ComparatorBuilder.new do
|
157
|
+
comparator :asdf do |a,b|
|
158
|
+
true
|
159
|
+
end
|
160
|
+
end
|
161
|
+
Scram::DSL::Definitions.add_comparators(builder)
|
162
|
+
```
|
163
|
+
|
164
|
+
Now your targets can use `asdf` as a conditions key, and Scram will use your method of comparison to determine if something is true or not. In this case, `asdf` returns true regardless of the two objects being compared.
|
165
|
+
## Development
|
166
|
+
|
167
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
168
|
+
|
169
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
170
|
+
|
171
|
+
## Contributing
|
172
|
+
|
173
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/skreem/scram.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Scram'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
require 'bundler/gem_tasks'
|
25
|
+
|
26
|
+
require 'rspec/core'
|
27
|
+
require 'rspec/core/rake_task'
|
28
|
+
|
29
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
30
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
31
|
+
|
32
|
+
task :default => :spec
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Scram
|
2
|
+
# Model to represent a Holder's permission policy
|
3
|
+
class Policy
|
4
|
+
include Mongoid::Document
|
5
|
+
|
6
|
+
embeds_many :targets, class_name: "Scram::Target"
|
7
|
+
|
8
|
+
validates_presence_of :name
|
9
|
+
|
10
|
+
# @return [String] Name for this Policy. Purely for organizational purposes.
|
11
|
+
field :name, type: String
|
12
|
+
|
13
|
+
# @return [String] What model this Policy applies to. Will be nil if this Policy is not bound to a model.
|
14
|
+
field :context, type: String
|
15
|
+
|
16
|
+
# @return [Integer] Priority to allow this policy to override another conflicting {Scram::Policy} opinion.
|
17
|
+
field :priority, type: Integer, default: 0
|
18
|
+
|
19
|
+
# Helper method to easily tell if this policy is bound to a model
|
20
|
+
# @note Unnecessary since we can just call model.nil?, but it is helpful nonetheless
|
21
|
+
# @return [Boolean] True if this Policy is bound to a model, false otherwise
|
22
|
+
def model?
|
23
|
+
return !model.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Attempts to constantize and get a model
|
27
|
+
# @return [Object, nil] An object, likely a {::Mongoid::Document}, that this policy is bound to. nil if there is none.
|
28
|
+
def model
|
29
|
+
begin
|
30
|
+
return Module.const_get(context)
|
31
|
+
rescue # NameError if context as a constant doesn't exist, TypeError if context nil
|
32
|
+
return nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Checks if a {Scram::Holder} can perform some action on an object by checking targets
|
37
|
+
# @param holder [Scram::Holder] The actor
|
38
|
+
# @param action [String] What the user is trying to do to obj
|
39
|
+
# @param obj [Object] The receiver of the action
|
40
|
+
# @return [Symbol] This policy's opinion on an action and object. :allow and :deny mean this policy has a target who explicitly defines
|
41
|
+
# its opinion, while :abstain means that none of the targets are applicable to the action, and so has no opinion.
|
42
|
+
def can? holder, action, obj
|
43
|
+
obj = obj.to_s if obj.is_a? Symbol
|
44
|
+
action = action.to_s
|
45
|
+
# Abstain if policy doesn't apply to the obj
|
46
|
+
if obj.is_a? String # String permissions
|
47
|
+
return :abstain if self.model? # abstain if policy doesn't handle strings
|
48
|
+
else # Model permissions
|
49
|
+
return :abstain if !self.model? # abstain if policy doesn't handle models
|
50
|
+
|
51
|
+
if obj.is_a?(Class) # Passed in a class, need to check based off the passed in model's name
|
52
|
+
return :abstain if self.context != obj.to_s # abstain if policy doesn't handle these types of models
|
53
|
+
else # Passed in an instance of a model, need to check based off the instance's class's name
|
54
|
+
return :abstain if self.context != obj.class.name
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Checks targets in priority order for explicit allow or deny.
|
59
|
+
targets.order_by([[:priority, :desc]]).each do |target|
|
60
|
+
opinion = target.can?(holder, action, obj)
|
61
|
+
return opinion if %i[allow deny].include? opinion
|
62
|
+
end
|
63
|
+
|
64
|
+
return :abstain
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Scram
|
2
|
+
# A broad Target interface to scope in a {Scram::Policy}. A target must either whitelist or deny through its allow attribute.
|
3
|
+
class Target
|
4
|
+
include Mongoid::Document
|
5
|
+
embedded_in :policy, class_name: "Scram::Policy"
|
6
|
+
|
7
|
+
# @return [Array] Actions which this target applies to
|
8
|
+
field :actions, type: Array, default: []
|
9
|
+
|
10
|
+
# @return [Hash] Conditions which this target filters through. Keys are comparators, values are hashes of fields and values to compare.
|
11
|
+
field :conditions, type: Hash, default: {}
|
12
|
+
|
13
|
+
# @return [Integer] Priority to allow this target to override another conflicting {Scram::Target} opinion.
|
14
|
+
field :priority, type: Integer, default: 0
|
15
|
+
|
16
|
+
# @return [Boolean] This target's modification onto a permission (allow or deny), i.e. its type
|
17
|
+
field :allow, type: Boolean, default: true
|
18
|
+
|
19
|
+
# @return [Symbol] The type of this target (either permissive as allow or deny)
|
20
|
+
def target_type
|
21
|
+
allow ? :allow : :deny
|
22
|
+
end
|
23
|
+
|
24
|
+
# Checks if a {Scram::Holder} can perform some action on an object given this target's conditions and allow-stance.
|
25
|
+
#
|
26
|
+
# Scram allows for special names:
|
27
|
+
# To make this target applicable to a string, use a key `*target_name` with value of the string permission.
|
28
|
+
# To compare a value to a holder, use `*holder` for a value. To compare a value to a custom condition defined by {Scram::DSL::Builders::ConditionBuilder}
|
29
|
+
# use an * before a key name (this name should match the one defined in your model).
|
30
|
+
# @param holder [Scram::Holder] The actor
|
31
|
+
# @param action [String] What the user is trying to do to obj
|
32
|
+
# @param obj [Object] The receiver of the action
|
33
|
+
# @return [Symbol] This target's opinion on an action and object. :allow and :deny mean this target explicitly defines
|
34
|
+
# its opinion, while :abstain means that this Target is not applicable to the action, and so has no opinion.
|
35
|
+
def can? holder, action, obj
|
36
|
+
obj = obj.to_s if obj.is_a? Symbol
|
37
|
+
action = action.to_s
|
38
|
+
|
39
|
+
return :abstain unless actions.include? action
|
40
|
+
|
41
|
+
# Handle String permissions
|
42
|
+
if obj.is_a? String
|
43
|
+
if obj == conditions[:equals][:'*target_name']
|
44
|
+
return target_type
|
45
|
+
else
|
46
|
+
return :abstain
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Model permissions
|
51
|
+
# Attempts to abstain by finding a condition or attribute where comparisons fail (and thus this target would be unapplicable)
|
52
|
+
conditions.each do |comparator_name, fields_hash|
|
53
|
+
comparator = Scram::DSL::Definitions::COMPARATORS[comparator_name.to_sym]
|
54
|
+
fields_hash.each do |field, model_value|
|
55
|
+
# Either gets the model's attribute or gets the DSL defined condition.
|
56
|
+
# Abstains if neither can be reached
|
57
|
+
attribute = begin obj.send(:"#{field}") rescue return :abstain end
|
58
|
+
|
59
|
+
# Special value substitutions
|
60
|
+
model_value.gsub! "*holder", holder.scram_compare_value if model_value.respond_to?(:gsub!)
|
61
|
+
|
62
|
+
# Abstain if this target doesn't apply to obj in any of its attributes
|
63
|
+
return :abstain unless comparator.call(attribute, model_value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
return target_type
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/scram.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "scram/engine"
|
2
|
+
|
3
|
+
require "scram/core_ext/symbol_extensions.rb"
|
4
|
+
require "scram/dsl/builders.rb"
|
5
|
+
require "scram/dsl/definitions.rb"
|
6
|
+
require "scram/dsl/model_conditions.rb"
|
7
|
+
require "scram/concerns/holder.rb"
|
8
|
+
require "scram/concerns/aggregate_holder.rb"
|
9
|
+
|
10
|
+
module Scram
|
11
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Scram
|
2
|
+
# Class representing a Holder of policies through other Holders
|
3
|
+
# @note Implementing classes must implement #aggregates and #scram_compare_value
|
4
|
+
module AggregateHolder
|
5
|
+
include Holder
|
6
|
+
|
7
|
+
# @return [Array] other holders to consider in Policy inclusion
|
8
|
+
def aggregates
|
9
|
+
raise NotImplementedError # should be implemented by subclass
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [Array] list of policies through aggregates
|
13
|
+
def policies
|
14
|
+
aggregate_policies = aggregates.map {|a| a.policies}.flatten.sort_by {|p| p.priority}.reverse
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Object] a value to compare {AggregateHolder} in the database. For example, an ObjectID would be suitable.
|
18
|
+
def scram_compare_value
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Scram
|
2
|
+
using SymbolExtensions
|
3
|
+
|
4
|
+
# Base class to represent a Holder of permissions through policies.
|
5
|
+
# @note Implementing classes must implement #policies and #scram_compare_value
|
6
|
+
module Holder
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# @return [Array] list of policies
|
10
|
+
def policies
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Object] a value to compare {Holder} in the database. For example, an ObjectID would be suitable.
|
15
|
+
def scram_compare_value
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Checks if this holder can perform some action on an object by checking the Holder's policies
|
20
|
+
# @param action [String] What the user is trying to do to obj
|
21
|
+
# @param obj [Object] The receiver of the action
|
22
|
+
# @return [Boolean] Whether or not holder can action to object. We define a full abstainment as a failure to perform the action.
|
23
|
+
def can? action, target
|
24
|
+
target = target.to_s if target.is_a? Symbol
|
25
|
+
action = action.to_s
|
26
|
+
|
27
|
+
# Checks policies in priority order for explicit allow or deny.
|
28
|
+
policies.sort_by(&:priority).reverse.each do |policy|
|
29
|
+
opinion = policy.can?(self, action, target)
|
30
|
+
return opinion.to_bool if %i[allow deny].include? opinion
|
31
|
+
end
|
32
|
+
|
33
|
+
return false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|