kantox-roles 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5120829150bcbfcb385b48107800baf61da72262
4
+ data.tar.gz: 756e3b82936790094c8d00ed90ff16e8d0610f7c
5
+ SHA512:
6
+ metadata.gz: 96fd86d02e2c44e7b51c108c4e4f9fabb0fabe306dd5b38e12f2fc40fdcefd896bf95131dc5a1ab8a9627e5a5018a0b0e41eea213021dc58f7757d828ec93891
7
+ data.tar.gz: 17b617b94f4df54811ca013132aae12f812cd1cb0cde138fadda24e53d03879249453069bb3bedff6acdd33fedbbd9045061b51ca0b4b66ba89215449ea078c0
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kantox-roles.gemspec
4
+ gemspec
@@ -0,0 +1,224 @@
1
+ # Kantox::Roles
2
+
3
+ Kantox Roles is the library to transparently handle an authorization. It is
4
+ fully backend-agnostic. Current implementation contains a working example
5
+ of pundit wrapping.
6
+
7
+ The main goal is to separate the wheat from the chaff and not to pollute
8
+ model/controller classes with authorization stuff.
9
+
10
+ With Kantox Roles one is to define the rules in one or more yaml files:
11
+
12
+ ```yaml
13
+ 'Kantox::Managed':
14
+ :yo : 'aspect'
15
+ :yo2 : :aspect
16
+ :yo3 :
17
+ :runner: 'Kantox::Strategies::Wrapper#wrap'
18
+ :yo5 :
19
+ :lambda: '->(context, im) { 42 }'
20
+ :yo6 :
21
+ :kantox_managedhandler:
22
+ :params: ['param1', 'param2']
23
+ 'Kantox::WildManaged':
24
+ 'y*' : :aspect
25
+ ```
26
+
27
+ There are four different kinds of handlers available:
28
+
29
+ * **simple** — like `aspect` above. There should be `Kantox::Strategies#aspect`
30
+ module function available. It will be called with parameters `context` and
31
+ `im` supplying the context instance (usually an instance of guarded controller
32
+ class) and the name of the guarded method in `Module::Class#method` notation.
33
+ The aforementioned function should _raise an exception_ of type
34
+ `Kantox::Strategies::StrategyError` whether the authority check is not passed.
35
+ * **runner** — the most powerful yet complicated guard. Should have the
36
+ executable module function as parameter in `Module::Class#method` notation.
37
+ This function will be called, yielding _`context`, `im` **and** the guarded
38
+ method, converted to `proc`_ as parameters. The typical usage:
39
+
40
+ ```ruby
41
+ def my_runner context, params = nil
42
+ fail Kantox::Strategies::MyRunnerError unless check_passed
43
+ params[:user] = :demo if demo_mode
44
+ params[:credit_card].gsub /\d/, 'X'
45
+ yield *params if block_given?
46
+ end
47
+ ```
48
+
49
+ As seen above, the full control over context is provided by this guard.
50
+
51
+ * **lambda** — the simple lambda, getting `context` and `im`. Is actually
52
+ a syntax sugar for the _simple_ guard
53
+ * **object** — used when there is a need to pass complicated parameters and/or
54
+ some other stuff to the guard. The class `Kantox::Managedhandler` must have
55
+ a constructor, accepting one argument (the hash of parameters) and `to_proc`
56
+ method, accepting two arguments (`context`, `im`). The class will be
57
+ instantiated with a parameters list and `to_proc` would be called on context.
58
+
59
+ ## Methods to guard
60
+
61
+ Wildcard notation is allowed. That said, `'y*'` will guard all the methods
62
+ starting with `y`, and `'*'` will guard everything on the respective class.
63
+
64
+ ## Deny ⇒ Allow
65
+
66
+ As soon as a method guard is specified in yaml file, the method is considered
67
+ as guarded. If there was an error finding the guard (class not exists, method
68
+ can not be instantiated etc,) the authorization request will be _rejected_.
69
+
70
+ ## Rails integration
71
+
72
+ ```ruby
73
+ # controllers/admin/admin_controller.rb
74
+
75
+ require 'kantox/roles'
76
+ # Specify the top-parent class to guard
77
+ Kantox::Roles.init Admin::AdminController
78
+ # load strategies
79
+ Dir['strategies/**/*.yml'].each do |f|
80
+ Kantox::Roles.configure f
81
+ end
82
+ # load stopwords for logger
83
+ Kantox::Helpers.logger_stopwords File.join 'strategies', 'stopwords.txt'
84
+ Kantox::Helpers.info "Strategies were read: #{Kantox::Roles.options}"
85
+ ```
86
+
87
+ ```ruby
88
+ # config/application.rb
89
+
90
+ # Policies are to be loaded on init explicitly
91
+ Dir[File.expand_path('../../app/policies/**/*', __FILE__)].each do |f|
92
+ require f[/(.*?)\.rb$/, 1] unless File.directory? f
93
+ end
94
+ ```
95
+
96
+ ```yaml
97
+ # strategiest/roles.yml
98
+
99
+ 'Admin::TodosController' :
100
+ '*' : :pundit
101
+ ...
102
+ ```
103
+
104
+ ## Pundit plug-in
105
+
106
+ This section shows how to integrate `pundit` to act as backend guard.
107
+ Everything one needs is to implement policies.
108
+
109
+ Everything in `TodosController` is to be handled by pundit. So, it’s time
110
+ to implement our `pundit` guard. Besides some syntax sugar stuff it is (complete implementation for `kantox-flow` may be seen [here](https://github.com/kantox/kantox-roles/wiki/Pundit-Policy):
111
+
112
+ ```ruby
113
+ # app/policies/pundit.rb
114
+
115
+ def pundit context, im
116
+ begin # Whoever does not reply on classify would be punished :)
117
+ model = context.instance_eval 'controller_path.classify'
118
+ policy = PolicyFactory.lookup model.split('::').last
119
+ unless policy.new(context.current_user, model).send("#{im.split('#').last}?")
120
+ fail PunditError.new(context, im, policy)
121
+ end
122
+ rescue NameError => e
123
+ Kantox::Helpers.err "Error punditing «#{context}». Will reject request.\nOriginal error: #{e}"
124
+ throw PunditError.new(context, im)
125
+ end
126
+ end
127
+ module_function :pundit
128
+ ```
129
+ Needless to say, the above handler is to be written once per backend. Pundit one is shipped with `Kantox::Roles`. The last but not least is to specify a strategy:
130
+
131
+ ```ruby
132
+ # app/policies/todo_policy.rb
133
+
134
+ module Kantox
135
+ module Policies
136
+ def historic?
137
+ @user.admin? and [true, false].sample
138
+ end
139
+ alias_method :index?, :historic?
140
+ end
141
+ end
142
+ end
143
+ ```
144
+
145
+ The above will randomly accept admin’s requests to a controller. I am pretty
146
+ sure one would do more sophisticated check here.
147
+
148
+ That’s it.
149
+
150
+ ## Policies Generator
151
+
152
+ `Kantox::Roles` has it’s generator for creating policies.
153
+
154
+ ### Generator invocation
155
+
156
+ ```
157
+ $ bundle exec rails generate kantox:pundit_policy Todo --users Administrator SalesPerson
158
+ ```
159
+
160
+ The above will generate (assuming that `Todo` is a proper model name and `TodosController` exists:
161
+
162
+ * Policy itself, in `app/policies`,
163
+ * Spec for a policy in `spec/policies`,
164
+ * Default strategy in `strategies`.
165
+ * [optional] if this is a first run, the `pundit:install` generator will be invoked
166
+ to generate default pundit `ApplicationPolicy`.
167
+
168
+ By default all the public controller methods will be guarded with `method?` pundit guards,
169
+ returning `true` if the current user was listed during generation process.
170
+
171
+ ![Generate Policy](https://github.com/kantox/kantox-flow/wiki/kantox-roles-generator-1.png)
172
+
173
+ ### Generated specs
174
+
175
+ The generator above would generate smart specs, more or less ready to use. For
176
+ an example above, it would generate specs, checking whether _all the specified
177
+ users are **allowed**_ to access the controller, and _all others are restricted_.
178
+
179
+ ![Generated Specs](https://github.com/kantox/kantox-flow/wiki/kantox-roles-generator-2.png)
180
+
181
+ ### Vandal-proof
182
+
183
+ The generator will gracefully reject a request to damage anything as well as
184
+ to generate policies for inexisting controller.
185
+
186
+ ![Vandal-proof](https://github.com/kantox/kantox-flow/wiki/kantox-roles-generator-3.png)
187
+
188
+ ## Installation
189
+
190
+ ```ruby
191
+ gem 'kantox-roles'
192
+ ```
193
+
194
+ ## TODOs
195
+
196
+ * generate policies and rspecs by `yaml`
197
+ * ~~pointcuts *syntax* — DSL vs YAML/JSON~~
198
+ * automatic *tests* for pointcuts — by adding them to descriptions
199
+ * allow⇒deny vs **deny⇒allow**
200
+ * ~~*supersupervisor* to change rights of supervisors~~
201
+ * ~~*groups* with *roles*, or smth morre sophisticated? What?~~
202
+ * ~~where to store rights? 3rd party?~~
203
+ * *edit rights* from the interface.
204
+
205
+ ## Usage
206
+
207
+ Add a line to `configuration` file (or explicitly by calling `Kantox::Roles.configure` with either hash, or passing a block.)
208
+
209
+ Optionally, one may specify a custom handler as denoted by `:runner` key in config.
210
+
211
+
212
+ ## Development
213
+
214
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
215
+
216
+ 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` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
217
+
218
+ ## Contributing
219
+
220
+ 1. Fork it ( https://github.com/[my-github-username]/kantox-roles/fork )
221
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
222
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
223
+ 4. Push to the branch (`git push origin my-new-feature`)
224
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "kantox/roles"
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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kantox/roles/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.required_ruby_version = '~> 2.1'
8
+
9
+ spec.name = 'kantox-roles'
10
+ spec.version = Kantox::Roles::VERSION
11
+ spec.authors = ['Kantox LTD']
12
+ spec.email = ['aleksei.matiushkin@kantox.com']
13
+ spec.license = 'Kantox LTD Commercial'
14
+
15
+ spec.summary = 'OmniRoles mechanism for Kantox Role Management'
16
+ spec.description = 'Roles Management interface for virtually every backend, mostly like OmniAuth for authentication'
17
+ spec.homepage = 'http://kantox.com'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(/(test|spec|features)\//) }
20
+ spec.bindir = 'bin'
21
+ spec.executables = spec.files.grep(/^exe\//) { |f| File.basename(f) }
22
+ spec.require_paths = %w(lib)
23
+
24
+ if spec.respond_to?(:metadata)
25
+ # spec.metadata['allowed_push_host'] = 'http://fury.io'
26
+ end
27
+
28
+ spec.add_dependency 'hashie', '~> 3'
29
+ spec.add_dependency 'pundit'
30
+
31
+ spec.add_development_dependency 'bundler', '~> 1.7'
32
+ spec.add_development_dependency 'rake', '~> 10.0'
33
+
34
+ spec.add_development_dependency 'pry', '~> 0.10'
35
+
36
+ spec.add_development_dependency 'rspec', '~> 2.12'
37
+ spec.add_development_dependency 'cucumber', '~> 1.3'
38
+ spec.add_development_dependency 'yard', '~> 0'
39
+ # spec.add_development_dependency 'yard-cucumber', '~> 0'
40
+ end
@@ -0,0 +1,199 @@
1
+ require 'hashie'
2
+
3
+ Dir[File.expand_path('../**/*', __FILE__)].each do |f|
4
+ require f[/(.*?)\.rb$/, 1] unless File.directory? f
5
+ end
6
+
7
+ module Kantox
8
+ LOCK_EMOJI = '🔒'
9
+
10
+ module Exceptions
11
+ class StandardError < ::StandardError; end
12
+ class NotAuthorized < StandardError; end
13
+ class NotAllowed < NotAuthorized; end
14
+ end
15
+
16
+ # This error is called when a programmer was as lame as to code wrong
17
+ class LameError < RuntimeError ; end
18
+
19
+ module Roles
20
+ class << self
21
+ attr_reader :controller
22
+
23
+ # Main initializer. This method is to be called before any controller
24
+ # is instantiated to prevent leakage of controllers handled.
25
+ # @param controller [Class] the class of main controller to handle
26
+ def init controller
27
+ return @controller if @controller == controller
28
+
29
+ fail LameError.new("Roles were already initialized for [#{@controller}]. Tried: [#{controller}]") \
30
+ unless @controller.nil? ||
31
+ # The following must satisfy zeus. Zeus reloads controller class
32
+ # making a comparision to fail. This is an ugly hack to f*ck zeus.
33
+ Rails.env.development? &&
34
+ !ENV['ZEUS_MASTER_FD'].nil? &&
35
+ @controller.name == controller.name
36
+
37
+ @controller = controller
38
+ if @controller.method(:inherited).owner == @controller
39
+ Kantox::Helpers.warn "#{controller}#inherited was already defined. Realiasing."
40
+ # FIXME @controller.send :alias_method, "∃inherited", :inherited
41
+ end
42
+
43
+ @controller.class_eval '
44
+ def self.inherited subclass
45
+ Kantox::Roles.postpone_strategies subclass
46
+ end
47
+ '
48
+ end
49
+
50
+ # Configures Roles by hash or yaml from string or file. Whether code block
51
+ # is passed, it is processed with @options instance.
52
+ # @param hos [String|Hash] the input data to configure
53
+ def configure hos = nil
54
+ @options = Kantox::Helpers.merge_hash_or_string options, hos
55
+ yield @options if block_given?
56
+ # @options.select! { |_, v| v.is_a? Enumerator } # FIXME WARNING OR LIKE
57
+ apply_all_strategies # FIXME Do we need this?
58
+ @options
59
+ end
60
+
61
+ def apply_all_strategies
62
+ options.keys.each { |klazz| postpone_strategies klazz }
63
+ end
64
+
65
+ def postpone_strategies klazz
66
+ return if postponed[klazz.to_s] # FIXME FIXME FIXME
67
+
68
+ case klazz
69
+ when Class
70
+ (@postponed[klazz.to_s] = TracePoint.new(:end) do |tp|
71
+ if tp.self == klazz
72
+ apply_strategies klazz.to_s
73
+ Kantox::Helpers.info "Strategies successfully applied to #{klazz} (callback in inherited)."
74
+ tp.disable
75
+ end
76
+ end).enable
77
+ when String, Symbol
78
+ if Kernel.const_defined?(klazz)
79
+ apply_strategies klazz
80
+ else
81
+ (@postponed[klazz] = TracePoint.new(:end) do |tp|
82
+ if tp.self.name == klazz
83
+ apply_strategies klazz
84
+ Kantox::Helpers.info "Strategies successfully applied to #{klazz} (eager waiting)."
85
+ tp.disable
86
+ end
87
+ end).enable
88
+ end
89
+ else fail LameError.new("Postpone strategies were called with inknown parameter type [#{klazz} :: #{klazz.class}]")
90
+ end
91
+ end
92
+
93
+ def apply_strategies klazz
94
+ return if options[klazz].nil? # no strategies to apply
95
+ Kantox::Helpers.debug "Request to apply strategies to [#{klazz}]"
96
+
97
+ options[klazz].each do |m, strategies|
98
+ next unless Kernel.const_defined? klazz
99
+
100
+ Kernel.const_get(klazz).instance_methods.select do |im|
101
+ im =~ Regexp.new("\\A#{m.gsub('*', '(?:.*?)')}\\z")
102
+ end.each do |sim|
103
+ [*strategies].each do |strategy|
104
+ apply_strategy klazz, sim, strategy
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def apply_strategy klazz, m, strategy
111
+ km = "#{klazz}##{m}"
112
+ case applied[km]
113
+ when :success
114
+ Kantox::Helpers.debug("Trying to reapply #{strategy} to #{km}. «Skipped».")
115
+ return
116
+ when NilClass, :error
117
+ Kantox::Helpers.debug("Will apply strategy [#{strategy}] to [#{km}]")
118
+ else
119
+ fail LameError.new
120
+ end
121
+
122
+ if (im = Kantox::Helpers.get_instance_method(km)).nil?
123
+ @applied[km] = :error
124
+ return
125
+ end
126
+
127
+ pc = patch_code im, strategy
128
+ patch = "
129
+ def #{im[:method][:name]} *args
130
+ begin
131
+ #{pc}
132
+ rescue Kantox::Strategies::StrategyError => e
133
+ Kantox::Helpers.info '«Denied #{klazz}##{m}» due to #{strategy} strategy.'
134
+ raise Kantox::Exceptions::NotAllowed, e.message
135
+ rescue Kantox::Exceptions::NotAllowed => e
136
+ Kantox::Helpers.info '«Denied #{klazz}##{m}» (deep) due to #{strategy} strategy.'
137
+ raise Kantox::Exceptions::NotAllowed, e.message
138
+ rescue Kantox::Exceptions::StandardError => e
139
+ Kantox::Helpers.catched 'Guarged «#{klazz}##{m}» throws a [kantox] exception.', e
140
+ raise e
141
+ rescue ::StandardError => e
142
+ Kantox::Helpers.catched 'Guarged «#{klazz}##{m}» throws a [generic] exception.', e
143
+ raise e
144
+ end
145
+ end
146
+ "
147
+ im[:class].send :alias_method, "∃#{im[:method][:name]}", "#{im[:method][:name]}"
148
+ im[:class].class_eval patch
149
+ applied[km] = :success
150
+ end
151
+
152
+ def patch_code im, strategy
153
+ Kantox::Helpers.debug "Strategy: #{strategy.class} :: [#{strategy}] "
154
+ args_as_string = im[:params][:string].empty? ? '' : '*args'
155
+
156
+ case strategy
157
+ when String, Symbol
158
+ "
159
+ strategy = Kantox::Helpers.get_simple_strategy('#{strategy}')
160
+ fail Kantox::Strategies::StrategyError(nil, '#{im[:method][:name]}') if strategy.nil?
161
+ strategy.call(self, '#{im[:class]}##{im[:method][:name]}', *args)
162
+ ∃#{im[:method][:name]} #{args_as_string}
163
+ "
164
+ when Array
165
+ case strategy.first.to_s
166
+ when 'runner'
167
+ "
168
+ strategy = Kantox::Helpers.get_#{strategy.first}_strategy('#{strategy.last}')
169
+ fail Kantox::Strategies::StrategyError(nil, '#{im[:method][:name]}') if strategy.nil?
170
+ p = self.method('∃#{im[:method][:name]}').to_proc
171
+ strategy.call(self, *args, &p)
172
+ "
173
+ when 'lambda'
174
+ "
175
+ strategy = Kantox::Helpers.get_#{strategy.first}_strategy('#{strategy.last}')
176
+ fail Kantox::Strategies::StrategyError(nil, '#{im[:method][:name]}') if strategy.nil?
177
+ strategy.call(self, '#{im[:class]}##{im[:method][:name]}')
178
+ ∃#{im[:method][:name]} #{args_as_string}
179
+ "
180
+ else
181
+ # Will try to instantiate the class with a given name
182
+ "
183
+ strategy = Kantox::Helpers.get_object_strategy('#{strategy.first}', '#{strategy.last}')
184
+ fail Kantox::Strategies::StrategyError(nil, '#{im[:method][:name]}') if strategy.nil?
185
+ strategy.to_proc.call(self, '#{im[:class]}##{im[:method][:name]}')
186
+ ∃#{im[:method][:name]} #{args_as_string}
187
+ "
188
+ end
189
+ else
190
+ fail LameError.new "Unknown strategy: #{strategy}."
191
+ end
192
+ end
193
+
194
+ def applied ; @applied ||= {} ; end
195
+ def postponed ; @postponed ||= {} ; end
196
+ def options ; @options ||= Hashie::Mash.new ; end
197
+ end
198
+ end
199
+ end