ingress 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +298 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ingress.gemspec +24 -0
- data/lib/ingress/build_permissions_repository_for_role.rb +15 -0
- data/lib/ingress/copy_permissions_repository_into_role.rb +15 -0
- data/lib/ingress/permission_rule.rb +57 -0
- data/lib/ingress/permissions.rb +64 -0
- data/lib/ingress/permissions_dsl.rb +49 -0
- data/lib/ingress/permissions_repository.rb +97 -0
- data/lib/ingress/version.rb +3 -0
- data/lib/ingress.rb +2 -0
- data/script/console +14 -0
- data/script/setup +7 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 06ab79a2f02f52156f88da841da4a010386ad71f
|
4
|
+
data.tar.gz: 47c37172ee0415e0046c3998aae2f4cbdcc4828f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8286f4993ee6bfd2aeb60434c781bb271c7b56988ef4010171de7ae1b2b007240ece07b6ccaf068ffaf788099a03743bc4735b12dd30128e8096b3424b1d7d67
|
7
|
+
data.tar.gz: b7875313e10cbe3a91a7593c0d942a0bb16dd44158c827659eabf4120d147eefcb3534f44b212c759431fb481028ae252976abd543d1b14d454b09da7bc2da91
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Alan Skorkin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
# Ingress
|
2
|
+
|
3
|
+
A simple role based authorization framework inspired by CanCan (similar syntax) with a nicer interface for defining the permissions for the roles in your system.
|
4
|
+
|
5
|
+
The biggest problem I had with CanCan was the fact that it mostly forced you define the permissions for all the roles in one class (really one method). And when the set of permissions in your system grew very large, you had to bend over backwards to allow you to break things down.
|
6
|
+
|
7
|
+
In the OO world we're used to being able to break down functionality into multiple smaller classes which we can them compose into a greater whole. This is the main idea behind this gem, keep the nice syntax that CanCan had, but allow composing the main permission object in your system from many smaller classes.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'ingress'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install ingress
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Let's say you have a user object in your system and the user can have multiple roles. Our set of roles will be `guest`, `member`, `admin`.
|
28
|
+
|
29
|
+
First we create the main permission object in our system, let's call it `UserPermissions`:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
class UserPermissions < Ingress::Permissions
|
33
|
+
def user_role_identifiers
|
34
|
+
user.roles.map do |role|
|
35
|
+
role.name.to_sym
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
A couple of things of note is that it inherits from `Ingress::Permissions`, from which it inherits the initializer:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
attr_reader :user
|
45
|
+
|
46
|
+
def initialize(user)
|
47
|
+
@user = user
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
So this object is always instantiated with a user. The second thing to note is that we have to provide a method called
|
52
|
+
`user_role_identifiers` which needs to return a list of role identifier that this particular user has. Above we make the
|
53
|
+
assumptions that a user has many roles and that a role has a name. So we iterate over the roles collect the symbolized names
|
54
|
+
and return them. This is essentially what ties everything together. We haven't defined any permissions just yet, but we
|
55
|
+
can already do the following:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
user_permissions = UserPermissions.new(user)
|
59
|
+
user_permissions.can?(:do, :stuff) # returns false
|
60
|
+
```
|
61
|
+
|
62
|
+
So now we have an object that we can instantiate anywhere, and ask it if our user has a particular permission.
|
63
|
+
Let us now define some permissions for a role. We'll start with the guest role. First, let's update our `UserPermissions`
|
64
|
+
object:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class UserPermissions < Ingress::Permissions
|
68
|
+
define_role_permissions :guest, GuestPermissions
|
69
|
+
|
70
|
+
def user_role_identifiers
|
71
|
+
user.roles.map do |role|
|
72
|
+
role.name.to_sym
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
We've now said that the permission for the role with the `guest` identifier live in the `GuestPermissions` class. Let's create
|
79
|
+
it:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class GuestPermissions < Ingress::Permissions
|
83
|
+
define_role_permissions do
|
84
|
+
can :view, :non_sensitive_info
|
85
|
+
can [:create], :session
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
It's pretty self explanatory, the class again inherits from `Ingress::Permissions` as that's where the simple DSL for defining
|
91
|
+
permissions lives. The thing to note is that we called the class `GuestPermissions`, but it could be called anything, the permissions
|
92
|
+
we define here are not attached to any role. They only get attached to the role via the `define_role_permissions :guest, GuestPermissions`
|
93
|
+
line in the `UserPermissions` class. The syntax for defining permissions is:
|
94
|
+
|
95
|
+
```
|
96
|
+
can 'action', 'subject'
|
97
|
+
or
|
98
|
+
cannot 'action', 'subject'
|
99
|
+
```
|
100
|
+
|
101
|
+
Similar to CanCan, the `action` can be any string, symbol or array of strings or symbols. The `subject` can also be a string or symbol, or
|
102
|
+
it can be a class constant. Let's define permissions for the next role in our system, `member` which is more complex. Firstly, update our `UserPermissions`.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
class UserPermissions < Ingress::Permissions
|
106
|
+
define_role_permissions :guest, GuestPermissions
|
107
|
+
define_role_permissions :member, MemberPermissions
|
108
|
+
|
109
|
+
def user_role_identifiers
|
110
|
+
user.roles.map do |role|
|
111
|
+
role.name.to_sym
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
Simple, next `MemberPermissions` class:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class MemberPermissions < Ingress::Permissions
|
121
|
+
define_role_permissions do
|
122
|
+
can [:show, :update, :destroy], :session
|
123
|
+
can :accept, :terms
|
124
|
+
can [:view, :create], Post
|
125
|
+
can [:update, :destroy], Post, if: ->(user, post) do
|
126
|
+
user.id == post.user_id
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
It's a little bit more complex, but still fairly self explanatory. As you can see, we have a `Post` object in our system. So we allow
|
133
|
+
user with a `member` role to view and create posts, and they can update and destroy posts that they own. So we could do:
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
user_permissions = UserPermissions.new(user)
|
137
|
+
user_permissions.can?(:create, Post) # returns true
|
138
|
+
post = user.posts.first # assume we can get the list of posts form the user object
|
139
|
+
user_permissions.can?(:update, post) # returns true
|
140
|
+
```
|
141
|
+
|
142
|
+
The condition lambda always takes two parameters, the `user` and an `object`, the object is whatever we supply to the `can?` method,
|
143
|
+
when we check permissions.
|
144
|
+
|
145
|
+
Let's add our admin role:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
class UserPermissions < Ingress::Permissions
|
149
|
+
define_role_permissions :guest, GuestPermissions
|
150
|
+
define_role_permissions :member, MemberPermissions
|
151
|
+
define_role_permissions :admin, AdminPermissions
|
152
|
+
|
153
|
+
def user_role_identifiers
|
154
|
+
user.roles.map do |role|
|
155
|
+
role.name.to_sym
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
And the class:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class AdminPermissions < Ingress::Permissions
|
165
|
+
define_role_permissions do
|
166
|
+
can "*", "*" # you can also use can_do_anything
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
As you can see both `action` and `subject` can be wildcards, so in this case an admin would be able to do anything in the system, i.e.
|
172
|
+
any call to `can?` will always return `true`.
|
173
|
+
|
174
|
+
So what else can we do? Well let's say we wanted another role called `limited_admin` which would be similar to admin, but can't destroy
|
175
|
+
comments:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
class UserPermissions < Ingress::Permissions
|
179
|
+
define_role_permissions :guest, GuestPermissions
|
180
|
+
define_role_permissions :member, MemberPermissions
|
181
|
+
define_role_permissions :admin, AdminPermissions
|
182
|
+
define_role_permissions :limited_admin, LimitedAdminPermissions
|
183
|
+
|
184
|
+
def user_role_identifiers
|
185
|
+
user.roles.map do |role|
|
186
|
+
role.name.to_sym
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
And the class:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
class LimitedAdminPermissions < Ingress::Permissions
|
196
|
+
inherits AdminPermissions
|
197
|
+
|
198
|
+
define_role_permissions do
|
199
|
+
cannot :destroy, Comment
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
So basically, we can inherit permissions that are defined in other classes, and either switch off some or add others. Let's create
|
205
|
+
some sort of `super_member` role, which can do everything a member can do, but can also update anything in the system:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
class UserPermissions < Ingress::Permissions
|
209
|
+
define_role_permissions :guest, GuestPermissions
|
210
|
+
define_role_permissions :member, MemberPermissions
|
211
|
+
define_role_permissions :admin, AdminPermissions
|
212
|
+
define_role_permissions :limited_admin, LimitedAdminPermissions
|
213
|
+
define_role_permissions :super_member, SuperMemberPermissions
|
214
|
+
|
215
|
+
def user_role_identifiers
|
216
|
+
user.roles.map do |role|
|
217
|
+
role.name.to_sym
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
And the class:
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class SuperMemberPermissions < Ingress::Permissions
|
227
|
+
inherits MemberPermissions
|
228
|
+
|
229
|
+
define_role_permissions do
|
230
|
+
can :update, "*"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
We can inherit permissions, and we use a wildcard subject, to allow a user with the `super_member` role to be able to update anything. We
|
236
|
+
can even define a common set of permissions which we want multiple roles to share and have the permission class for each of those roles
|
237
|
+
inherit from the common set. Let's say we want a `financial_officer` role and a `reporting_officer` role both of which should have the ability to do anything with a `Transaction` object in our system (for whatever reason):
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
class UserPermissions < Ingress::Permissions
|
241
|
+
define_role_permissions :guest, GuestPermissions
|
242
|
+
define_role_permissions :member, MemberPermissions
|
243
|
+
define_role_permissions :admin, AdminPermissions
|
244
|
+
define_role_permissions :limited_admin, LimitedAdminPermissions
|
245
|
+
define_role_permissions :super_member, SuperMemberPermissions
|
246
|
+
define_role_permissions :financial_officer, FinancialOfficerPermissions
|
247
|
+
define_role_permissions :reporting_officer, ReportingOfficerPermissions
|
248
|
+
|
249
|
+
def user_role_identifiers
|
250
|
+
user.roles.map do |role|
|
251
|
+
role.name.to_sym
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
And the classes:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
class CommonPermissions < Ingress::Permissions
|
261
|
+
define_role_permissions do
|
262
|
+
can "*", Transaction
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class FinancialOfficerPermissions < Ingress::Permissions
|
267
|
+
inherits CommonPermissions
|
268
|
+
end
|
269
|
+
|
270
|
+
class ReportingOfficerPermissions < Ingress::Permissions
|
271
|
+
inherits CommonPermissions
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
Now we wildcard the action, so we can do anything to `Transaction` objects. And we have to other sets of permission inherit from
|
276
|
+
the `CommonPermissions` class.
|
277
|
+
|
278
|
+
I hope it's relatively clear that it's pretty flexible, you can almost endlessly decompose the permission definitions into smaller classes
|
279
|
+
then combine via `inherits` and assign the final permission set to a role identifier via `define_role_permissions` on the main
|
280
|
+
`UserPermissions` class.
|
281
|
+
|
282
|
+
So now the authorization in your system can be defined in a much more OO way, without nasty and complex tricks. And you can still enjoy a nice syntax very similar to CanCan.
|
283
|
+
|
284
|
+
This framework has no hooks into Rails (these would be trivial to write if necessary, e.g. you can instantiate the `user_permissions` object on your `ApplicationController` and then do the `can?` checks anywhere you want) and can therefore be used with any web framework, or even outside of the context of a web framework (if such a use case makes sense).
|
285
|
+
|
286
|
+
## Development
|
287
|
+
|
288
|
+
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.
|
289
|
+
|
290
|
+
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).
|
291
|
+
|
292
|
+
## Contributing
|
293
|
+
|
294
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ingress.
|
295
|
+
|
296
|
+
## License
|
297
|
+
|
298
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ingress"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/ingress.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ingress/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ingress"
|
8
|
+
spec.version = Ingress::VERSION
|
9
|
+
spec.authors = ["Alan Skorkin"]
|
10
|
+
spec.email = ["alan@skorks.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Simple role based authorization for Ruby applications}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "bin"
|
18
|
+
spec.executables = spec.files.grep(%r{^script/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "ingress/permissions_dsl"
|
2
|
+
|
3
|
+
module Ingress
|
4
|
+
module Services
|
5
|
+
class BuildPermissionsRepositoryForRole
|
6
|
+
class << self
|
7
|
+
def perform(role_identifier, &block)
|
8
|
+
permissions_dsl = PermissionsDsl.new(role_identifier)
|
9
|
+
permissions_dsl.instance_eval(&block)
|
10
|
+
permissions_dsl.permission_repository
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "ingress/permissions_repository"
|
2
|
+
|
3
|
+
module Ingress
|
4
|
+
module Services
|
5
|
+
class CopyPermissionsRepositoryIntoRole
|
6
|
+
class << self
|
7
|
+
def perform(role_identifier, template_permission_repository)
|
8
|
+
permission_repository = PermissionsRepository.new
|
9
|
+
permission_repository.copy_to_role(role_identifier, template_permission_repository)
|
10
|
+
permission_repository
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Ingress
|
2
|
+
class PermissionRule
|
3
|
+
attr_reader :action, :subject, :conditions
|
4
|
+
|
5
|
+
def initialize(allows:, action:, subject:, conditions: nil)
|
6
|
+
@allows = allows
|
7
|
+
@action = action
|
8
|
+
@subject = subject
|
9
|
+
@conditions = [conditions].compact.flatten
|
10
|
+
end
|
11
|
+
|
12
|
+
def allows?
|
13
|
+
@allows
|
14
|
+
end
|
15
|
+
|
16
|
+
def match?(given_action, given_subject, user)
|
17
|
+
return false unless action_matches?(given_action)
|
18
|
+
return false unless subject_matches?(given_subject)
|
19
|
+
|
20
|
+
conditions_match?(user, given_subject)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def action_matches?(given_action)
|
26
|
+
given_action == action ||
|
27
|
+
given_action == "*" ||
|
28
|
+
"*" == action
|
29
|
+
end
|
30
|
+
|
31
|
+
def subject_matches?(given_subject)
|
32
|
+
given_subject == subject ||
|
33
|
+
given_subject.class == subject ||
|
34
|
+
given_subject == "*" ||
|
35
|
+
"*" == subject
|
36
|
+
end
|
37
|
+
|
38
|
+
def conditions_match?(user, given_subject)
|
39
|
+
conditions.all? do |condition|
|
40
|
+
condition.call(user, given_subject)
|
41
|
+
end
|
42
|
+
rescue => e
|
43
|
+
log_error(e)
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_error(error)
|
48
|
+
if defined?(Rails)
|
49
|
+
Rails.logger.error error.message
|
50
|
+
Rails.logger.error error.backtrace.join("\n")
|
51
|
+
else
|
52
|
+
$stderr.puts error.message
|
53
|
+
$stderr.puts error.backtrace.join("\n")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "ingress/permissions_repository"
|
2
|
+
require "ingress/copy_permissions_repository_into_role"
|
3
|
+
require "ingress/build_permissions_repository_for_role"
|
4
|
+
|
5
|
+
module Ingress
|
6
|
+
class Permissions
|
7
|
+
class << self
|
8
|
+
def permissions_repository
|
9
|
+
@permissions_repository ||= PermissionsRepository.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def inherits(permissions_class)
|
13
|
+
role_identifier = :dummy
|
14
|
+
|
15
|
+
if permissions_class
|
16
|
+
@permissions_repository = permissions_repository.merge(
|
17
|
+
Services::CopyPermissionsRepositoryIntoRole.perform(role_identifier, permissions_class.permissions_repository),
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def define_role_permissions(role_identifier = nil, permissions_class = nil, &block)
|
23
|
+
if role_identifier.nil?
|
24
|
+
role_identifier = :dummy
|
25
|
+
end
|
26
|
+
|
27
|
+
if permissions_class
|
28
|
+
@permissions_repository = permissions_repository.merge(
|
29
|
+
Services::CopyPermissionsRepositoryIntoRole.perform(role_identifier, permissions_class.permissions_repository),
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
if block_given?
|
34
|
+
@permissions_repository = permissions_repository.merge(Services::BuildPermissionsRepositoryForRole.perform(role_identifier, &block))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :user
|
40
|
+
|
41
|
+
def initialize(user)
|
42
|
+
@user = user
|
43
|
+
end
|
44
|
+
|
45
|
+
def can?(action, subject)
|
46
|
+
find_matching_rules(action, subject).any? do |rule|
|
47
|
+
rule.match?(action, subject, user)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_role_identifiers
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def find_matching_rules(action, subject)
|
58
|
+
user_role_identifiers.reduce([]) do |rules, role_identifier|
|
59
|
+
rules += self.class.permissions_repository.rules_for(role_identifier, action, subject)
|
60
|
+
rules
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "ingress/permissions_repository"
|
2
|
+
|
3
|
+
module Ingress
|
4
|
+
class PermissionsDsl
|
5
|
+
attr_reader :role_identifier, :permission_repository
|
6
|
+
|
7
|
+
def initialize(role_identifier)
|
8
|
+
@role_identifier = role_identifier
|
9
|
+
@permission_repository = PermissionsRepository.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def can_do_anything
|
13
|
+
permission_repository.add_permission(role_identifier, true, "*", "*")
|
14
|
+
end
|
15
|
+
|
16
|
+
def can(actions, subjects, options = {}, &block)
|
17
|
+
for_each_action_and_subject(actions, subjects) do |action, subject|
|
18
|
+
condition = condition_from_options(options, block)
|
19
|
+
permission_repository.add_permission(role_identifier, true, action, subject, condition)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def cannot(actions, subjects, options = {}, &block)
|
24
|
+
for_each_action_and_subject(actions, subjects) do |action, subject|
|
25
|
+
condition = condition_from_options(options, block)
|
26
|
+
permission_repository.add_permission(role_identifier, false, action, subject, condition)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def for_each_action_and_subject(actions, subjects)
|
33
|
+
return unless block_given?
|
34
|
+
actions = [actions].flatten
|
35
|
+
subjects = [subjects].flatten
|
36
|
+
|
37
|
+
actions.each do |action|
|
38
|
+
subjects.each do |subject|
|
39
|
+
yield(action, subject)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def condition_from_options(options, block)
|
45
|
+
if_condition = options[:if] || block
|
46
|
+
if_condition.respond_to?(:call) ? if_condition : nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "ingress/permission_rule"
|
2
|
+
|
3
|
+
module Ingress
|
4
|
+
class PermissionsRepository
|
5
|
+
attr_reader :role_rules
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@role_rules = Hashes.role_rules
|
9
|
+
@role_subject_action_rule = Hashes.role_subject_action_rule
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_permission(role_identifier, allow, action, subject, conditions = nil)
|
13
|
+
rule = PermissionRule.new(allows: allow, action: action, subject: subject, conditions: conditions)
|
14
|
+
add_rule(role_identifier, rule)
|
15
|
+
end
|
16
|
+
|
17
|
+
def rules_for(role_identifier, action, subject)
|
18
|
+
rules = []
|
19
|
+
|
20
|
+
rules += find_rules(role_identifier, action, subject)
|
21
|
+
rules += find_rules(role_identifier, "*", "*")
|
22
|
+
rules += find_rules(role_identifier, action, "*")
|
23
|
+
rules += find_rules(role_identifier, "*", subject)
|
24
|
+
rules = apply_negative_rules(rules)
|
25
|
+
|
26
|
+
rules
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge(permission_repository)
|
30
|
+
permission_repository.role_rules.each_pair do |role_identifier, rules|
|
31
|
+
rules.each do |rule|
|
32
|
+
add_rule(role_identifier, rule)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def copy_to_role(role_identifier, permission_repository)
|
39
|
+
permission_repository.role_rules.each_pair do |_, rules|
|
40
|
+
rules.each do |rule|
|
41
|
+
add_rule(role_identifier, rule)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def apply_negative_rules(rules)
|
50
|
+
# remove rules that cancel each other out since we're within the
|
51
|
+
# context of 1 role, i.e. if any rule is a negation it will
|
52
|
+
# cancel all other ones
|
53
|
+
if rules.any? { |rule| !rule.allows? }
|
54
|
+
[]
|
55
|
+
else
|
56
|
+
rules
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_rules(role_identifier, action, subject)
|
61
|
+
rules = []
|
62
|
+
rules += @role_subject_action_rule[role_identifier][subject][action]
|
63
|
+
unless subject == "*"
|
64
|
+
rules += @role_subject_action_rule[role_identifier][subject.class][action]
|
65
|
+
end
|
66
|
+
|
67
|
+
rules
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_rule(role_identifier, rule)
|
71
|
+
@role_rules[role_identifier] << rule
|
72
|
+
@role_subject_action_rule[role_identifier][rule.subject][rule.action] << rule
|
73
|
+
end
|
74
|
+
|
75
|
+
# creates hashes with the default values required in
|
76
|
+
# PermissionsRepository's constructor
|
77
|
+
module Hashes
|
78
|
+
module_function
|
79
|
+
|
80
|
+
# Three level deep hash returning an array
|
81
|
+
def role_subject_action_rule
|
82
|
+
Hash.new do |hash1, key1|
|
83
|
+
hash1[key1] = Hash.new do |hash2, key2|
|
84
|
+
hash2[key2] = Hash.new do |hash3, key3|
|
85
|
+
hash3[key3] = []
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# One level deep hash returning an array
|
92
|
+
def role_rules
|
93
|
+
Hash.new { |hash, key| hash[key] = [] }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/ingress.rb
ADDED
data/script/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ingress"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/script/setup
ADDED
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ingress
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alan Skorkin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- alan@skorks.com
|
58
|
+
executables:
|
59
|
+
- console
|
60
|
+
- setup
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- ".gitignore"
|
65
|
+
- ".rspec"
|
66
|
+
- ".ruby-version"
|
67
|
+
- ".travis.yml"
|
68
|
+
- Gemfile
|
69
|
+
- LICENSE.txt
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- bin/console
|
73
|
+
- bin/setup
|
74
|
+
- ingress.gemspec
|
75
|
+
- lib/ingress.rb
|
76
|
+
- lib/ingress/build_permissions_repository_for_role.rb
|
77
|
+
- lib/ingress/copy_permissions_repository_into_role.rb
|
78
|
+
- lib/ingress/permission_rule.rb
|
79
|
+
- lib/ingress/permissions.rb
|
80
|
+
- lib/ingress/permissions_dsl.rb
|
81
|
+
- lib/ingress/permissions_repository.rb
|
82
|
+
- lib/ingress/version.rb
|
83
|
+
- script/console
|
84
|
+
- script/setup
|
85
|
+
homepage: ''
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
requirements: []
|
104
|
+
rubyforge_project:
|
105
|
+
rubygems_version: 2.5.1
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Simple role based authorization for Ruby applications
|
109
|
+
test_files: []
|