picky_guard 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dae068b49826c41bc05bdfcc15ddb02cc31fe69c
4
+ data.tar.gz: 828592f4204f3cde8748ac1be0e61a56a4466525
5
+ SHA512:
6
+ metadata.gz: 68bc8fbca4bd9931c65c2dfd5b5c1a08e83450b6f4974bdd5d132c3e57e7b45b3f8088d0ebc0225a507231dcdb278157d931495a036cee65206d29f7d33bb7fb
7
+ data.tar.gz: 800cba86587b463f310444fe3b15d9ea24bb28ccebc297a57abc1b72dc9c444c7648cf2df97bb3d9da62f032c8bac70eb3e47a87bcdd232cc87c51fe9783ef5b
data/.gitignore ADDED
@@ -0,0 +1,87 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.idea/
10
+
11
+
12
+ # Created by https://www.gitignore.io/api/rubymine
13
+
14
+ ### RubyMine ###
15
+ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
16
+ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
17
+
18
+ # User-specific stuff:
19
+ .idea/**/workspace.xml
20
+ .idea/**/tasks.xml
21
+ .idea/dictionaries
22
+
23
+ # Sensitive or high-churn files:
24
+ .idea/**/dataSources/
25
+ .idea/**/dataSources.ids
26
+ .idea/**/dataSources.xml
27
+ .idea/**/dataSources.local.xml
28
+ .idea/**/sqlDataSources.xml
29
+ .idea/**/dynamic.xml
30
+ .idea/**/uiDesigner.xml
31
+
32
+ # Gradle:
33
+ .idea/**/gradle.xml
34
+ .idea/**/libraries
35
+
36
+ # CMake
37
+ cmake-build-debug/
38
+
39
+ # Mongo Explorer plugin:
40
+ .idea/**/mongoSettings.xml
41
+
42
+ ## File-based project format:
43
+ *.iws
44
+
45
+ ## Plugin-specific files:
46
+
47
+ # IntelliJ
48
+ /out/
49
+
50
+ # mpeltonen/sbt-idea plugin
51
+ .idea_modules/
52
+
53
+ # JIRA plugin
54
+ atlassian-ide-plugin.xml
55
+
56
+ # Cursive Clojure plugin
57
+ .idea/replstate.xml
58
+
59
+ # Ruby plugin and RubyMine
60
+ /.rakeTasks
61
+
62
+ # Crashlytics plugin (for Android Studio and IntelliJ)
63
+ com_crashlytics_export_strings.xml
64
+ crashlytics.properties
65
+ crashlytics-build.properties
66
+ fabric.properties
67
+
68
+ ### RubyMine Patch ###
69
+ # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
70
+
71
+ # *.iml
72
+ # modules.xml
73
+ # .idea/misc.xml
74
+ # *.ipr
75
+
76
+ # Sonarlint plugin
77
+ .idea/sonarlint
78
+
79
+
80
+ # End of https://www.gitignore.io/api/rubymine
81
+
82
+ # Created by https://www.gitignore.io/api/idea
83
+
84
+ #!! ERROR: idea is undefined. Use list command to see defined gitignore types !!#
85
+
86
+
87
+ # End of https://www.gitignore.io/api/idea
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ Documentation:
2
+ Enabled: false
3
+ Metrics/LineLength:
4
+ Max: 100
5
+ Metrics/BlockLength:
6
+ Exclude:
7
+ - "**/*_spec.rb"
8
+ Metrics/ModuleLength:
9
+ Exclude:
10
+ - "**/*_spec.rb"
11
+ Metrics/MethodLength:
12
+ Max: 5
13
+ Exclude:
14
+ - "**/*_spec.rb"
15
+ Metrics/ParameterLists:
16
+ Max: 4
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+ Initial deployment.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in picky_guard.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ picky_guard (0.1.0)
5
+ activerecord
6
+ cancancan
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (5.2.0)
12
+ activesupport (= 5.2.0)
13
+ activerecord (5.2.0)
14
+ activemodel (= 5.2.0)
15
+ activesupport (= 5.2.0)
16
+ arel (>= 9.0)
17
+ activesupport (5.2.0)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 0.7, < 2)
20
+ minitest (~> 5.1)
21
+ tzinfo (~> 1.1)
22
+ arel (9.0.0)
23
+ cancancan (2.2.0)
24
+ concurrent-ruby (1.0.5)
25
+ diff-lcs (1.3)
26
+ i18n (1.0.0)
27
+ concurrent-ruby (~> 1.0)
28
+ minitest (5.11.3)
29
+ rake (10.5.0)
30
+ rspec (3.7.0)
31
+ rspec-core (~> 3.7.0)
32
+ rspec-expectations (~> 3.7.0)
33
+ rspec-mocks (~> 3.7.0)
34
+ rspec-core (3.7.1)
35
+ rspec-support (~> 3.7.0)
36
+ rspec-expectations (3.7.0)
37
+ diff-lcs (>= 1.2.0, < 2.0)
38
+ rspec-support (~> 3.7.0)
39
+ rspec-mocks (3.7.0)
40
+ diff-lcs (>= 1.2.0, < 2.0)
41
+ rspec-support (~> 3.7.0)
42
+ rspec-support (3.7.1)
43
+ sqlite3 (1.3.13)
44
+ thread_safe (0.3.6)
45
+ tzinfo (1.2.5)
46
+ thread_safe (~> 0.1)
47
+
48
+ PLATFORMS
49
+ ruby
50
+
51
+ DEPENDENCIES
52
+ bundler (~> 1.16)
53
+ picky_guard!
54
+ rake (~> 10.0)
55
+ rspec (~> 3.2)
56
+ sqlite3
57
+
58
+ BUNDLED WITH
59
+ 1.16.1
data/README.md ADDED
@@ -0,0 +1,285 @@
1
+ # PickyGuard
2
+
3
+ PickyGuard is an opinionated authorization library which wraps [CanCanCan](https://github.com/CanCanCommunity/cancancan).
4
+
5
+ This library helps to write authorization policies in an opinionated hierarchy.
6
+
7
+ Briefly,
8
+ * **User** has many **roles**.
9
+ * Each **role** has many **policies**.
10
+ * Each **policy** has many **statements** describing which **actions** has what **effect** on which **resources**.
11
+
12
+ For example,
13
+ * User `Paul` has a role named `CampaignManager`.
14
+ * The role `CampaignManager` has a policy named `crud_all_campaigns`.
15
+ * The policy `crud_all_campaigns` means,
16
+ * Actions : `[:read, :update, :create, :delete]` are
17
+ * Effect : `Allow`ed
18
+ * Resources : for `All campaigns under user's company`.
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'picky_guard'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install picky_guard
35
+
36
+ ## Usage
37
+
38
+ To generate initial files, execute:
39
+
40
+ ```
41
+ $ rails generate picky_guard:install
42
+ ```
43
+
44
+ This will create the following files:
45
+
46
+ ```
47
+ app/
48
+ - models/
49
+ - ability.rb
50
+ - picky_guard/
51
+ - role_policies.rb
52
+ - resource_actions.rb
53
+ - user_role_checker.rb
54
+ ```
55
+
56
+ ## Generated Files
57
+
58
+ ### ability.rb
59
+
60
+ The generated file is like this:
61
+
62
+ ```ruby
63
+ class Ability < PickyGuard::Loader
64
+ def initialize(user)
65
+ adjust(user, UserRoleChecker, ResourceActions, RolePolicies)
66
+ end
67
+ end
68
+ ```
69
+
70
+ Normally, you don't have to do anything about it.
71
+
72
+ ### user_role_checker.rb
73
+
74
+ The generated file is like this:
75
+
76
+ ```ruby
77
+ class UserRoleChecker < PickyGuard::UserRoleChecker
78
+ def self.check(user, role)
79
+ # ...
80
+ end
81
+ end
82
+ ```
83
+
84
+ This class defines the way to check if user has specific role. It assumes some roles already have been given to the user somehow.
85
+
86
+ You can implement this on your own, or if you're using a gem like [rolify](https://github.com/RolifyCommunity/rolify), then it should be like this:
87
+
88
+ ```ruby
89
+ class UserRoleChecker < PickyGuard::UserRoleChecker
90
+ def self.check(user, role)
91
+ user.has_role? role
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### resource_actions.rb
97
+
98
+ The generated file is like this:
99
+
100
+ ```ruby
101
+ class ResourceActions < PickyGuard::ResourceActions
102
+ def initialize
103
+ map(Report, [:create, :read, :update, :delete])
104
+ end
105
+ end
106
+ ```
107
+
108
+ This class defines which resource can have which actions. Actions can be an array of either `String` or `Symbol`.
109
+
110
+ By defining this, you can explicitly manage list of actions per resource and filter out unexpected and unknown actions.
111
+
112
+ ### role_policies.rb
113
+
114
+ The generated file is like this:
115
+
116
+ ```ruby
117
+ class RolePolicies < PickyGuard::RolePolicies
118
+ def initialize
119
+ map(:role_report_manager, [ManageAllReports])
120
+ # map(:role_report_reader, [AnotherPolicy])
121
+ end
122
+ end
123
+ ```
124
+
125
+ This class defines which role has which policies. From the example code above, we could assume there is a role named `:role_report_manager` and it has one policy named `ManageAllReports`.
126
+
127
+ Then how do we define policy?
128
+
129
+ ## Defining Policies
130
+
131
+ To generate new policy, execute this:
132
+
133
+ ```
134
+ $ rails generate picky_guard:policy manage_all_reports
135
+ ```
136
+
137
+ From the command line, name should be underscored. Otherwise, it will raise an error.
138
+
139
+ Once created, you will find the policy file under `app/picky_guard/policies/`.
140
+
141
+ If you get to have many policies, you can group them into a folder like this:
142
+
143
+ ```
144
+ $ rails generate picky_guard:policy reports/manage_all_reports
145
+ ```
146
+
147
+ Then it will generate `app/picky_guard/policies/reports/manage_all_reports.rb`.
148
+
149
+ The generated file is like this:
150
+
151
+ ```ruby
152
+ class ManageAllReports < PickyGuard::Policy
153
+ def initialize(current_user)
154
+ statement_for Campaign do
155
+ allow
156
+ actions [:create]
157
+ conditions({})
158
+ end
159
+
160
+ statement_for Campaign do
161
+ allow
162
+ actions [:create]
163
+ class_resource
164
+ end
165
+ end
166
+ end
167
+ ```
168
+
169
+ `register` method takes a parameter and a block.
170
+ * The parameter is a resource class. It should extend `ActiveRecord::Base`.
171
+ * The block consists of simple DSL, describing the statement.
172
+
173
+ ### Building Statement
174
+
175
+ There are two types of resources: `instance resource` and `class resource`.
176
+
177
+ ```ruby
178
+ can? :read, Campaign.first # Checking permission against an instance resource
179
+
180
+ can? :create, Campaign # Checking permission against a class resource
181
+ ```
182
+
183
+ ### Instance Resource
184
+
185
+ In case of `instance resource`, we need
186
+ * effect(`allow` or `deny`)
187
+ * actions
188
+ * conditions
189
+
190
+ ```ruby
191
+ statement_for Campaign do # Instances of `Campaign` are the resources.
192
+ allow # Possibly `deny` instead of `allow`. If omitted, it's `allow` by default.
193
+ actions [:create] # Array of `string` or `symbol`.
194
+ instance_resource # If omitted, it's an instance resource by default.
195
+ conditions({})
196
+ end
197
+ ```
198
+
199
+ In a short way,
200
+
201
+ ```ruby
202
+ statement_for Campaign do
203
+ actions [:create]
204
+ conditions({})
205
+ end
206
+ ```
207
+
208
+ ### Class Resource
209
+
210
+ In case of `class resource`, we need
211
+ * effect
212
+ * actions
213
+ * class_resource
214
+
215
+ ```ruby
216
+ statement_for Campaign do # `Campaign` is the resource.
217
+ allow # Possibly `deny` instead of `allow`. If omitted, it's `allow` by default.
218
+ actions [:create] # Array of `string` or `symbol`.
219
+ class_resource # You need this explicit declaration when it comes to a class resource.
220
+ end
221
+ ```
222
+
223
+ You cannot specify any conditions on class resource.
224
+
225
+ In a short way,
226
+
227
+ ```ruby
228
+ statement_for Campaign do
229
+ actions [:create]
230
+ class_resource
231
+ end
232
+ ```
233
+
234
+ ### `conditions` on instance resource
235
+
236
+ `conditions` is a hash. This is directly used to query database, so it should be real database column names. You can refer to `Hash of Conditions` section from [Defining Abilities - CanCanCan](https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities#hash-of-conditions).
237
+
238
+ When things are too complicated and it's hard to express it a hash, then there's a little detour.
239
+
240
+ ```ruby
241
+ ids = extract_campaign_ids_somehow
242
+ statement_for Campaign do
243
+ actions [:create]
244
+ conditions({ id: ids })
245
+ end
246
+ ```
247
+
248
+ First, you can extract ids or other values through some complicated business logic of yours. Then, pass it to conditions like the above.
249
+
250
+ However we can make this better by wrapping the conditions with `proc`. This enables lazy-loading.
251
+
252
+ ```ruby
253
+ statement_for Campaign do
254
+ actions [:create]
255
+ conditions(proc {
256
+ ids = extract_campaign_ids_somehow
257
+
258
+ { id: ids }
259
+ })
260
+ end
261
+ ```
262
+
263
+ So basically this `conditions` method takes `a hash` or `a proc returning a hash` as a parameter.
264
+
265
+ ## Using `Ability`
266
+
267
+ You can use `Ability` class just as you did with `CanCanCan`. The constructor takes one parameter: `user`.
268
+
269
+ With `PickyGuard`, you can pass optional second parameter which is `resource`.
270
+
271
+ ```ruby
272
+ Ability.new(user, Campaign).can? :read, Campaign.first
273
+ ```
274
+
275
+ This will load only relevant policies.
276
+
277
+ ## Development
278
+
279
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
280
+
281
+ 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).
282
+
283
+ ## Contributing
284
+
285
+ Bug reports and pull requests are welcome on GitHub at https://github.com/eunjae-lee/picky_guard.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require 'bundler/setup'
6
+ require 'picky_guard'
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require 'irb'
16
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/db/schema.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define do
4
+ create_table 'apps', force: true do |t|
5
+ t.integer 'status1'
6
+ t.integer 'status2'
7
+ t.integer 'status3'
8
+ end
9
+
10
+ create_table 'campaigns', force: true do
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PickyGuard
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ # rubocop:disable Metrics/LineLength
9
+ def generate_install
10
+ copy_file 'ability.rb', 'app/models/ability.rb'
11
+ copy_file 'role_policies.rb', 'app/picky_guard/role_policies.rb'
12
+ copy_file 'resource_actions.rb', 'app/picky_guard/resource_actions.rb'
13
+ copy_file 'user_role_checker.rb', 'app/picky_guard/user_role_checker.rb'
14
+ end
15
+ # rubocop:enable Metrics/LineLength
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PickyGuard
4
+ module Generators
5
+ class PolicyGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ desc 'Generates a policy with the given NAME'
9
+
10
+ def generate_policy
11
+ return unless validate_name(name)
12
+ create_file dest_path(name), content(name)
13
+ end
14
+
15
+ private
16
+
17
+ def dest_path(name)
18
+ "app/picky_guard/policies/#{name}.rb"
19
+ end
20
+
21
+ def content(name)
22
+ class_name = class_name(name)
23
+ puts "class_name : #{class_name}"
24
+ path = File.join(File.expand_path('../templates', __FILE__), 'policy.rb.erb')
25
+ ERB.new(File.read(path)).result binding
26
+ end
27
+
28
+ def class_name(name)
29
+ name.split('/').last.camelize
30
+ end
31
+
32
+ def validate_name(name)
33
+ return true if underscored?(name)
34
+ puts_name_requirement(name)
35
+ end
36
+
37
+ def puts_name_requirement(name)
38
+ puts ''
39
+ puts 'Name should be underscored'
40
+ puts "Expected : #{name.underscore}"
41
+ puts "Actual : #{name}"
42
+ end
43
+
44
+ def underscored?(name)
45
+ name.underscore == name
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/loader'
4
+
5
+ class Ability < PickyGuard::Loader
6
+ def initialize(user)
7
+ adjust(user, UserRoleChecker, ResourceActions, RolePolicies)
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/policy'
4
+
5
+ class <%= class_name %> < PickyGuard::Policy
6
+ def initialize(current_user)
7
+ # register(App, proc {
8
+ # PickyGuard::StatementBuilder.new
9
+ # .allow
10
+ # .actions(%w[Create Read Update Delete])
11
+ # .resource(App)
12
+ # .conditions({})
13
+ # .build
14
+ # })
15
+
16
+ # register(Campaign, PickyGuard::StatementBuilder.new
17
+ # .allow
18
+ # .actions(%w[Create])
19
+ # .class_resource(Campaign)
20
+ # .build)
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/resource_actions'
4
+
5
+ class ResourceActions < PickyGuard::ResourceActions
6
+ def initialize
7
+ # map(Report, %w[Create Read Update Delete])
8
+
9
+ # [App, Campaign].each do |resource|
10
+ # map(resource, %w[Create Read Update])
11
+ # end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/role_policies'
4
+
5
+ class RolePolicies < PickyGuard::RolePolicies
6
+ def initialize
7
+ map(:role_report_manager, [ManageAllReports])
8
+ # map(:role_report_reader, [AnotherPolicy])
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/user_role_checker'
4
+
5
+ class UserRoleChecker < PickyGuard::UserRoleChecker
6
+ def self.check(user, role)
7
+ # ...
8
+ end
9
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cancancan'
4
+
5
+ module PickyGuard
6
+ class Loader
7
+ include CanCan::Ability
8
+
9
+ def initialize(user, *resources_whitelist)
10
+ @resources_whitelist = resources_whitelist
11
+ end
12
+
13
+ def adjust(user, user_role_checker_class, resource_actions_class, role_policies_class)
14
+ validate_parameters(user_role_checker_class, resource_actions_class, role_policies_class)
15
+ policies = gather_policies(user, user_role_checker_class, role_policies_class.new)
16
+ statements = gather_statements(user, policies, resource_actions_class.new)
17
+ adjust_statements(statements)
18
+ end
19
+
20
+ private
21
+
22
+ def validate_parameters(user_role_checker_class, resource_actions_class, role_policies_class)
23
+ raise ArgumentError unless user_role_checker_class < PickyGuard::UserRoleChecker
24
+ raise ArgumentError unless role_policies_class < PickyGuard::RolePolicies
25
+ raise ArgumentError unless resource_actions_class < PickyGuard::ResourceActions
26
+ end
27
+
28
+ def adjust_statements(statements)
29
+ statements.each do |statement|
30
+ adjust_statement(statement)
31
+ end
32
+ end
33
+
34
+ def adjust_statement(statement)
35
+ statement.actions.each do |action|
36
+ rule = build_rule(action, statement)
37
+ add_rule(rule)
38
+ end
39
+ end
40
+
41
+ def build_rule(action, statement)
42
+ conditions = eval_conditions_if_needed(statement)
43
+ CanCan::Rule.new(positive?(statement.effect), action, statement.resource, conditions, nil)
44
+ end
45
+
46
+ def eval_conditions_if_needed(statement)
47
+ if statement.conditions.is_a? Proc
48
+ statement.conditions.call
49
+ else
50
+ statement.conditions
51
+ end
52
+ end
53
+
54
+ def positive?(effect)
55
+ effect == Statement::EFFECT_ALLOW
56
+ end
57
+
58
+ def gather_policies(user, user_role_checker_class, role_policies)
59
+ role_policies.roles
60
+ .select { |role| user_role_checker_class.check(user, role) }
61
+ .map { |role| role_policies.policies_for(role) }
62
+ .flatten
63
+ end
64
+
65
+ def gather_statements(user, policies, resource_actions)
66
+ policies.map { |policy_class| policy_class.new(user) }
67
+ .map do |policy|
68
+ statements = policy.statements(@resources_whitelist)
69
+ validate_statements!(resource_actions, statements)
70
+ end.flatten
71
+ end
72
+
73
+ def validate_statements!(resource_actions, statements)
74
+ statements.each do |statement|
75
+ validate_statement!(resource_actions, statement)
76
+ end
77
+ statements
78
+ end
79
+
80
+ def validate_statement!(resource_actions, statement)
81
+ statement.actions.each do |action|
82
+ valid = resource_actions.action_exist?(statement.resource, action)
83
+ raise 'Unknown action!' unless valid
84
+ end
85
+ statement
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PickyGuard
4
+ class PathHelper
5
+ def self.user_project_path
6
+ Dir.pwd
7
+ end
8
+
9
+ def self.lib_path
10
+ File.expand_path(File.join(project_root, 'lib'))
11
+ end
12
+
13
+ def self.project_root
14
+ File.expand_path(File.join(File.dirname(__FILE__), '../..'))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/validator'
4
+ require 'picky_guard/statement_proxy'
5
+
6
+ module PickyGuard
7
+ class Policy
8
+ def initialize(current_user)
9
+ # do nothing here
10
+ end
11
+
12
+ def statements(resource_whitelist)
13
+ @cached_statements ||= gather_statements(resource_whitelist)
14
+ end
15
+
16
+ def statement_for(resource, &statement_definition)
17
+ proxy = StatementProxy.new(resource)
18
+ proxy.instance_eval(&statement_definition)
19
+ register(resource, proxy.build)
20
+ end
21
+
22
+ private
23
+
24
+ def register(resource, statement)
25
+ safe_array << [resource, statement]
26
+ @cached_statements = nil
27
+ end
28
+
29
+ def gather_statements(resource_whitelist)
30
+ filtered_array(resource_whitelist).map do |_resource, statement|
31
+ Validator.validate_statement!(statement)
32
+ end
33
+ end
34
+
35
+ def filtered_array(resource_whitelist)
36
+ return safe_array if resource_whitelist.empty?
37
+
38
+ safe_array.select { |item| resource_whitelist.include? item[0] }
39
+ end
40
+
41
+ def safe_array
42
+ (@statements ||= [])
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/validator'
4
+
5
+ module PickyGuard
6
+ class ResourceActions
7
+ def map(resource, actions)
8
+ validate_parameters(actions, resource)
9
+ safe_hash[resource] = actions
10
+ end
11
+
12
+ def action_exist?(resource, action)
13
+ raise 'Unknown resource!' if safe_hash[resource].nil?
14
+ safe_hash[resource].include? action
15
+ end
16
+
17
+ private
18
+
19
+ def safe_hash
20
+ (@map ||= {})
21
+ end
22
+
23
+ def validate_parameters(actions, resource)
24
+ Validator.validate_resource_class!(resource)
25
+ Validator.validate_all_actions!(actions)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/validator'
4
+
5
+ module PickyGuard
6
+ class RolePolicies
7
+ def map(role, policies)
8
+ validate_parameters(policies, role)
9
+ safe_map[role] = policies
10
+ end
11
+
12
+ def roles
13
+ safe_map.keys
14
+ end
15
+
16
+ def policies_for(role)
17
+ safe_map[role]
18
+ end
19
+
20
+ private
21
+
22
+ def safe_map
23
+ (@map ||= {})
24
+ end
25
+
26
+ def validate_parameters(policies, role)
27
+ Validator.validate_all_policy_classes!(policies)
28
+ Validator.validate_role!(role)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/validator'
4
+
5
+ module PickyGuard
6
+ class Statement
7
+ attr_reader :effect, :actions, :resource, :conditions
8
+
9
+ EFFECT_ALLOW = :allow
10
+ EFFECT_DENY = :deny
11
+ EFFECTS = [EFFECT_ALLOW, EFFECT_DENY].freeze
12
+
13
+ RESOURCE_TYPE_INSTANCE = :instance
14
+ RESOURCE_TYPE_CLASS = :class
15
+
16
+ def initialize(effect, actions, resource, conditions)
17
+ @effect = effect
18
+ @actions = actions
19
+ @resource = resource
20
+ @conditions = conditions
21
+ end
22
+
23
+ def allow?
24
+ @effect == EFFECT_ALLOW
25
+ end
26
+
27
+ def deny?
28
+ @effect == EFFECT_DENY
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/statement'
4
+
5
+ module PickyGuard
6
+ class StatementProxy
7
+ def initialize(resource)
8
+ @resource = resource
9
+ allow
10
+ instance_resource
11
+ end
12
+
13
+ def allow
14
+ @effect = PickyGuard::Statement::EFFECT_ALLOW
15
+ end
16
+
17
+ def deny
18
+ @effect = PickyGuard::Statement::EFFECT_DENY
19
+ end
20
+
21
+ def actions(actions)
22
+ @actions = actions
23
+ end
24
+
25
+ def conditions(conditions)
26
+ @conditions = conditions
27
+ end
28
+
29
+ def instance_resource
30
+ @resource_type = PickyGuard::Statement::RESOURCE_TYPE_INSTANCE
31
+ end
32
+
33
+ def class_resource
34
+ @resource_type = PickyGuard::Statement::RESOURCE_TYPE_CLASS
35
+ end
36
+
37
+ def validate!
38
+ Validator.validate_effect!(@effect)
39
+ Validator.validate_all_actions!(@actions)
40
+ Validator.validate_resource_class!(@resource)
41
+ Validator.validate_conditions!(@conditions) if instance_resource?
42
+ end
43
+
44
+ def build
45
+ validate!
46
+ build_statement
47
+ end
48
+
49
+ private
50
+
51
+ def build_statement
52
+ PickyGuard::Statement.new(@effect, @actions, @resource, conditions_or_nil)
53
+ end
54
+
55
+ def conditions_or_nil
56
+ @conditions if instance_resource?
57
+ end
58
+
59
+ def instance_resource?
60
+ @resource_type == PickyGuard::Statement::RESOURCE_TYPE_INSTANCE
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PickyGuard
4
+ class UserRoleChecker
5
+ def self.check(user, role)
6
+ raise 'fill me'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/statement'
4
+ require 'picky_guard/policy'
5
+
6
+ module PickyGuard
7
+ class Validator
8
+ def self.valid_resource_class?(resource)
9
+ child_and_parent?(resource, ActiveRecord::Base)
10
+ end
11
+
12
+ def self.valid_role?(role)
13
+ role.is_a?(Symbol) || role.is_a?(String)
14
+ end
15
+
16
+ def self.valid_statement?(statement)
17
+ statement.is_a? PickyGuard::Statement
18
+ end
19
+
20
+ def self.valid_action?(action)
21
+ action.is_a?(Symbol) || action.is_a?(String)
22
+ end
23
+
24
+ def self.all_valid_actions?(actions)
25
+ return false unless actions.is_a? Array
26
+ actions.all? do |action|
27
+ valid_action?(action)
28
+ end
29
+ end
30
+
31
+ def self.valid_policy?(policy)
32
+ child_and_parent?(policy, PickyGuard::Policy)
33
+ end
34
+
35
+ def self.all_valid_policy_classes?(policies)
36
+ return false unless policies.is_a? Array
37
+ policies.all? do |policy|
38
+ valid_policy?(policy)
39
+ end
40
+ end
41
+
42
+ def self.valid_conditions?(conditions)
43
+ conditions.instance_of?(Hash) || conditions.instance_of?(Proc)
44
+ end
45
+
46
+ def self.child_and_parent?(child_class, parent_class)
47
+ child_class < parent_class
48
+ end
49
+
50
+ def self.valid_effect?(effect)
51
+ PickyGuard::Statement::EFFECTS.include? effect
52
+ end
53
+
54
+ def self.validate_statement!(statement)
55
+ raise ArgumentError, 'Invalid Statement' unless Validator.valid_statement?(statement)
56
+ statement
57
+ end
58
+
59
+ def self.validate_resource_class!(resource)
60
+ raise ArgumentError, 'Invalid Resource' unless Validator.valid_resource_class?(resource)
61
+ resource
62
+ end
63
+
64
+ def self.validate_all_actions!(actions)
65
+ raise ArgumentError, 'Invalid actions' unless Validator.all_valid_actions?(actions)
66
+ actions
67
+ end
68
+
69
+ def self.validate_all_policy_classes!(policies)
70
+ raise ArgumentError, 'Invalid policies' unless Validator.all_valid_policy_classes?(policies)
71
+ policies
72
+ end
73
+
74
+ def self.validate_role!(role)
75
+ raise ArgumentError, 'Invalid roles' unless Validator.valid_role?(role)
76
+ role
77
+ end
78
+
79
+ def self.validate_conditions!(conditions)
80
+ raise ArgumentError, 'Invalid conditions' unless Validator.valid_conditions?(conditions)
81
+ conditions
82
+ end
83
+
84
+ def self.validate_effect!(effect)
85
+ raise ArgumentError, 'Invalid effect' unless Validator.valid_effect?(effect)
86
+ effect
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PickyGuard
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'picky_guard/version'
4
+
5
+ module PickyGuard
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'picky_guard/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'picky_guard'
9
+ spec.version = PickyGuard::VERSION
10
+ spec.authors = ['Eunjae Lee']
11
+ spec.email = ['karis612@gmail.com']
12
+
13
+ spec.summary = 'PickyGuard is an opinionated authorization library.'
14
+ spec.description = spec.description
15
+ spec.homepage = 'https://github.com/eunjae-lee/picky_guard'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'activerecord'
25
+ spec.add_dependency 'cancancan'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.16'
28
+ spec.add_development_dependency 'rake', '~> 10.0'
29
+ spec.add_development_dependency 'rspec', '~> 3.2'
30
+ spec.add_development_dependency 'sqlite3'
31
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: picky_guard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eunjae Lee
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cancancan
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.2'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: ''
98
+ email:
99
+ - karis612@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".rubocop.yml"
107
+ - ".ruby-version"
108
+ - CHANGELOG.md
109
+ - Gemfile
110
+ - Gemfile.lock
111
+ - README.md
112
+ - Rakefile
113
+ - bin/console
114
+ - bin/setup
115
+ - db/schema.rb
116
+ - lib/generators/picky_guard/install_generator.rb
117
+ - lib/generators/picky_guard/policy_generator.rb
118
+ - lib/generators/picky_guard/templates/ability.rb
119
+ - lib/generators/picky_guard/templates/policy.rb.erb
120
+ - lib/generators/picky_guard/templates/resource_actions.rb
121
+ - lib/generators/picky_guard/templates/role_policies.rb
122
+ - lib/generators/picky_guard/templates/user_role_checker.rb
123
+ - lib/picky_guard.rb
124
+ - lib/picky_guard/loader.rb
125
+ - lib/picky_guard/path_helper.rb
126
+ - lib/picky_guard/policy.rb
127
+ - lib/picky_guard/resource_actions.rb
128
+ - lib/picky_guard/role_policies.rb
129
+ - lib/picky_guard/statement.rb
130
+ - lib/picky_guard/statement_proxy.rb
131
+ - lib/picky_guard/user_role_checker.rb
132
+ - lib/picky_guard/validator.rb
133
+ - lib/picky_guard/version.rb
134
+ - picky_guard.gemspec
135
+ homepage: https://github.com/eunjae-lee/picky_guard
136
+ licenses: []
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.6.11
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: PickyGuard is an opinionated authorization library.
158
+ test_files: []