kantox-roles 1.2.1

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.
@@ -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