dry-ability 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d5704a2a2bf2199fda53a0a74556d9f93c1cc82a46ef9d9912d2452f34fce4f
4
+ data.tar.gz: b9dd8b4243ad05c462fd14a2052c623bac572588941368f3943a91d6228ce548
5
+ SHA512:
6
+ metadata.gz: 816667eb2d315a2fda11b40df7e1a5bef065f4425cb4a6785e9bcf617c4761670b9b406f3ddfacef5d0ccde498ef9d226c2f6a4a66596ee8dee0b0c1f04a4944
7
+ data.tar.gz: de539543156d5b2b9878a0057f4e5271100c06d7d74d9fce7e5abe56215d85a6d7a68db281f258838eff31e6bf044d0b125b52de6b766a9d0392f6f6115a3cb6
data/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
data/.gitignore ADDED
@@ -0,0 +1,50 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+ *.bridgesupport
21
+ build-iPhoneOS/
22
+ build-iPhoneSimulator/
23
+
24
+ ## Specific to RubyMotion (use of CocoaPods):
25
+ #
26
+ # We recommend against adding the Pods directory to your .gitignore. However
27
+ # you should judge for yourself, the pros and cons are mentioned at:
28
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
29
+ #
30
+ # vendor/Pods/
31
+
32
+ ## Documentation cache and generated files:
33
+ /.yardoc/
34
+ /_yardoc/
35
+ /doc/
36
+ /rdoc/
37
+
38
+ ## Environment normalization:
39
+ /.bundle/
40
+ /vendor/bundle
41
+ /lib/bundler/man/
42
+
43
+ # for a library or gem, you might want to ignore these files since the code is
44
+ # intended to run in multiple environments; otherwise, check them in:
45
+ # Gemfile.lock
46
+ # .ruby-version
47
+ # .ruby-gemset
48
+
49
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
50
+ .rvmrc
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.5
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Anton
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Dry::Ability
2
+
3
+ Dry::Ability is an authorization library, which is trying to replace [cancancan](https://github.com/CanCanCommunity/cancancan) API. The goal is creating less secondary objects by performing authorization. Some codebase, like a controller extension, was copied from cancancan and slightly modified, but public API stays the same. By some reasons (especially, due to integration with InheritedResources) I forked it from version `1.17.0`.
4
+
5
+ One significant difference with CanCanCan is that an ability's rules a defined once on class definition and stored into `Dry::Container`.
6
+
7
+ Docs, specs & benchmarks is coming soon…
8
+
9
+
10
+ ## Installation
11
+
12
+ Add this to your Gemfile:
13
+
14
+ gem 'dry-ability'
15
+
16
+ and run the `bundle install` command.
17
+
18
+ ## Getting Started
19
+
20
+ Soon…
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'bundler/gem_tasks'
2
+ # require 'rspec/core/rake_task'
3
+ # require 'rubocop/rake_task'
4
+
5
+ # desc 'Run Rubocop'
6
+ # RuboCop::RakeTask.new
7
+ #
8
+ # desc 'Run RSpec'
9
+ # RSpec::Core::RakeTask.new do |t|
10
+ # t.verbose = false
11
+ # end
12
+ #
13
+ # task default: [:rubocop, :spec]
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'dry/ability/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'dry-ability'
9
+ s.version = Dry::Ability::VERSION
10
+ s.authors = ['Anton Semenov', 'Alessandro Rodi (Renuo AG)', 'Bryan Rite', 'Ryan Bates', 'Richard Wilson']
11
+ s.email = 'anton.estum@gmail.com'
12
+ s.homepage = 'https://github.com/estum/dry-ability'
13
+ s.summary = 'Dried authorization solution for Rails.'
14
+ s.description = 'Dried authorization solution for Rails. All permissions are stored in a single location.'
15
+ s.platform = Gem::Platform::RUBY
16
+ s.license = 'MIT'
17
+
18
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
+ s.test_files = `git ls-files -- Appraisals {spec,features,gemfiles}/*`.split($INPUT_RECORD_SEPARATOR)
20
+ s.executables = `git ls-files -- bin/*`.split($INPUT_RECORD_SEPARATOR).map { |f| File.basename(f) }
21
+ s.require_paths = ['lib']
22
+
23
+ s.required_ruby_version = '>= 2.6.0'
24
+
25
+ s.add_dependency 'activesupport', '>= 5.2'
26
+ s.add_dependency 'dry-types', '>= 1.5.0'
27
+ s.add_dependency 'dry-initializer', '>= 3.0.4'
28
+ s.add_dependency 'dry-container', '>= 0.7.2'
29
+ s.add_dependency 'concurrent-ruby', '>= 1.1.8'
30
+
31
+ s.add_development_dependency 'bundler', '~> 2.2.15'
32
+ s.add_development_dependency 'rubocop', '~> 0.46'
33
+ s.add_development_dependency 'rake', '~> 13.0.3'
34
+ s.add_development_dependency 'rspec', '~> 3.2.0'
35
+ s.add_development_dependency 'appraisal', '>= 2.0.0'
36
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'dry-ability'
@@ -0,0 +1 @@
1
+ require "dry/ability"
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/object/with_options'
5
+ require 'active_support/core_ext/module/delegation'
6
+ require 'active_support/core_ext/class/attribute'
7
+
8
+ require 'dry/ability/version'
9
+ require 'dry/ability/exceptions'
10
+ require "dry/ability/f"
11
+ require "dry/ability/t"
12
+ require "dry/ability/key"
13
+ require 'dry/ability/rules_builder'
14
+
15
+ module Dry
16
+ # Mixin class with DSL to define abilities
17
+ #
18
+ # @example
19
+ #
20
+ # class Ability
21
+ # include Dry::Ability.define -> do
22
+ # map_subject! :public => %w(Post Like Comment)
23
+ #
24
+ # map_action! :read => %i(index show),
25
+ # :create => %i(new),
26
+ # :update => %i(edit),
27
+ # :crud => %i(index create read show update destroy),
28
+ # :change => %i(update destroy)
29
+ #
30
+ # can :read, :public
31
+ # can :
32
+ #
33
+ # end
34
+ # end
35
+ module Ability
36
+ # @private
37
+ module DSL
38
+ def define(proc = nil, **options, &block)
39
+ rules = RulesBuilder.new(**options)
40
+ rules.instance_exec(&(proc || block))
41
+ [self, rules.mixin]
42
+ end
43
+ end
44
+
45
+ extend ActiveSupport::Concern
46
+ extend DSL
47
+
48
+ module ClassMethods
49
+ attr_reader :_container
50
+ alias_method :rules, :_container
51
+ end
52
+
53
+ attr_reader :account
54
+
55
+ def initialize(account)
56
+ @account = account
57
+ end
58
+
59
+ def authorize!(action, subject, message: nil)
60
+ if can?(action, subject)
61
+ subject
62
+ else
63
+ raise AccessDenied.new(message, action, subject)
64
+ end
65
+ end
66
+
67
+ def can?(action, subject)
68
+ rules = resolve_rules(action, subject) do
69
+ return false
70
+ end
71
+
72
+ rules.reduce(true) do |result, rule|
73
+ result && rule[@account, subject]
74
+ end
75
+ end
76
+
77
+ def cannot?(action, subject, *args)
78
+ !can?(action, subject, *args)
79
+ end
80
+
81
+ def attributes_for(action, subject)
82
+ rules = resolve_rules(action, subject) do
83
+ return {}
84
+ end
85
+ rules.reduce({}) do |result, rule|
86
+ result.merge!(rule.attributes_for(@account, subject)); result
87
+ end
88
+ end
89
+
90
+ def scope_for(action, subject)
91
+ rules = resolve_rules(action, subject) do
92
+ return yield if block_given?
93
+ if subject.respond_to?(:none)
94
+ return subject.none
95
+ else
96
+ raise ArgumentError, "expected subject to be an ActiveRecord::Base class or Relation. given: #{subject}"
97
+ end
98
+ end
99
+ if rules.none?(&:accessible?)
100
+ if block_given?
101
+ return yield
102
+ else
103
+ raise Error, "none of matched rules are provides scope for #{action}, #{subject}, pass block instead"
104
+ end
105
+ end
106
+ rules.map { |rule| rule.scope_for(@account, subject) }.reduce(:merge)
107
+ end
108
+
109
+ def resolve_rules(action, subject)
110
+ rules.resolve_with_mappings(action, subject) do |e|
111
+ Rails.logger.warn { e.message }
112
+ block_given? ? yield : nil
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ delegate :rules, to: "self.class"
119
+ end
120
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/container"
4
+ require "dry/ability/f"
5
+
6
+ module Dry
7
+ module Ability
8
+ class Container < Dry::Container
9
+ MAPPING_NSFN = {
10
+ :subject => F[:string_tpl, "mappings.subject.%s"].to_proc,
11
+ :action => F[:string_tpl, "mappings.action.%s"].to_proc
12
+ }
13
+ RULES_NSFN = F[:string_tpl, "rules.%s.%s"].to_proc
14
+
15
+ # @yieldparam exception
16
+ # Yields block with an instance of +RuleNotDefault+ exception class
17
+ def resolve_with_mappings(action, subject)
18
+ candidates = key_candidates(action, subject)
19
+ result = []
20
+ candidates.each do |key|
21
+ next unless key?(key)
22
+ result << resolve(key)
23
+ end
24
+ if result.blank?
25
+ exception = RuleNotDefined.new(action: action, subject: subject, candidates: candidates)
26
+ raise exception unless block_given?
27
+ yield(exception)
28
+ else
29
+ result
30
+ end
31
+ end
32
+
33
+ def key_candidates(action, subject)
34
+ subject, action = mappings(:subject, subject), mappings(:action, action)
35
+ subject.product(action).map!(&RULES_NSFN)
36
+ end
37
+
38
+ def mappings(kind, key)
39
+ F.collect_mappings(key, self, MAPPING_NSFN[kind], &:to_s)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/ability/resource_mediator"
4
+ require "dry/ability/controller/mixin"
5
+ require "dry/ability/controller/dsl"
6
+ require "dry/ability/controller_resource"
7
+ require "dry/ability/inherited_resource" if defined?(::InheritedResources)
8
+
9
+ module Dry
10
+ module Ability
11
+ module Controller
12
+ extend ActiveSupport::Concern
13
+
14
+ include Controller::Mixin
15
+
16
+ module ClassMethods
17
+ include Controller::DSL
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module Dry
6
+ module Ability
7
+ module Controller
8
+ module DSL
9
+ private def inherited(klass)
10
+ super(klass)
11
+ klass.instance_variable_set(:@_resource_mediators, @_resource_mediators.dup)
12
+ klass.instance_variable_set(:@_cancan_skipper, @_cancan_skipper.dup)
13
+ klass
14
+ end
15
+
16
+ # @api private
17
+ def set_resource_mediator_callback(name, **opts)
18
+ callback_options = opts.extract!(:only, :except, :if, :unless, :prepend)
19
+ @_resource_mediators ||= Concurrent::Map.new
20
+ @_resource_mediators.fetch_or_store(name) do
21
+ ResourceMediator.new(name, controller_path, __callee__, **opts).
22
+ tap { |m| before_action m, callback_options }
23
+ end.sequence << __callee__
24
+ end
25
+
26
+ # @!method load_and_authorize_resource(*args, **opts)
27
+ alias_method :load_and_authorize_resource, :set_resource_mediator_callback
28
+
29
+ # @!method load_resource(*args, **opts)
30
+ # @see #load_and_authorize_resource
31
+ # @option opts [Array<String,Symbol>] :only Only applies before filter to given actions.
32
+ # @option opts [Array<String,Symbol>] :except Does not apply before filter to given actions.
33
+ # @option opts [Boolean] :prepend Prepend callback
34
+ # @option opts [Symbol] :through Load this resource through another one.
35
+ # @option opts [Symbol] :through_association
36
+ # @option opts [Boolean] :shallow (false) allow this resource to be loaded directly when parent is +nil+
37
+ # @option opts [Boolean] :singleton (false) singleton resource through a +has_one+ association.
38
+ # @option opts [Boolean] :parent defaults to +true+ if a resource name is given which does not match the controller.
39
+ # @option opts [String,Class] :class The class to use for the model (string or constant).
40
+ # @option opts [String,Symbol] :instance_name The name of the instance variable to load the resource into.
41
+ # @option opts [Symbol] :find_by will use find_by(permalink: params[:id])
42
+ # @option opts [Symbol] :id_param Find using a param key other than :id. For example:
43
+ # @option opts [Array<Symbol>] :collection External actions as resource collection
44
+ # @option opts [Array<Symbol>] :new External actions as new resource
45
+ alias_method :load_resource, :set_resource_mediator_callback
46
+
47
+ # @!method authorize_resource(*args, **opts)
48
+ # @see #load_and_authorize_resource
49
+ # @option opts [Array<String,Symbol>] :only Only applies before filter to given actions.
50
+ # @option opts [Array<String,Symbol>] :except Does not apply before filter to given actions.
51
+ # @option opts [Boolean] :prepend Prepend callback
52
+ # @option opts [Boolean] :singleton (false) singleton resource through a +has_one+ association.
53
+ # @option opts [Boolean] :parent (true)
54
+ # @option opts [String,Class] :class The class to use for the model (string or constant).
55
+ # @option opts [String,Symbol] :instance_name The name of the instance variable to load the resource into.
56
+ # @option opts [Symbol] :through Authorize conditions on this parent resource when instance isn't available.
57
+ alias_method :authorize_resource, :set_resource_mediator_callback
58
+
59
+ undef_method :set_resource_mediator_callback
60
+
61
+ # Skip both the loading and authorization behavior of CanCan for this given controller. This is primarily
62
+ # useful to skip the behavior of a superclass. You can pass :only and :except options to specify which actions
63
+ # to skip the effects on. It will apply to all actions by default.
64
+ #
65
+ # class ProjectsController < SomeOtherController
66
+ # skip_load_and_authorize_resource :only => :index
67
+ # end
68
+ #
69
+ # You can also pass the resource name as the first argument to skip that resource.
70
+ def skip_load_and_authorize_resource(*args)
71
+ skip_load_resource(*args)
72
+ skip_authorize_resource(*args)
73
+ end
74
+
75
+ # Skip the loading behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
76
+ # only do authorization on certain actions. You can pass :only and :except options to specify which actions to
77
+ # skip the effects on. It will apply to all actions by default.
78
+ #
79
+ # class ProjectsController < ApplicationController
80
+ # load_and_authorize_resource
81
+ # skip_load_resource :only => :index
82
+ # end
83
+ #
84
+ # You can also pass the resource name as the first argument to skip that resource.
85
+ def skip_load_resource(name, **options)
86
+ cancan_skipper[:load][name] = options
87
+ end
88
+
89
+ # Skip the authorization behavior of CanCan. This is useful when using +load_and_authorize_resource+ but want to
90
+ # only do loading on certain actions. You can pass :only and :except options to specify which actions to
91
+ # skip the effects on. It will apply to all actions by default.
92
+ #
93
+ # class ProjectsController < ApplicationController
94
+ # load_and_authorize_resource
95
+ # skip_authorize_resource :only => :index
96
+ # end
97
+ #
98
+ # You can also pass the resource name as the first argument to skip that resource.
99
+ def skip_authorize_resource(name, **options)
100
+ cancan_skipper[:authorize][name] = options
101
+ end
102
+
103
+ # Add this to a controller to ensure it performs authorization through +authorized+! or +authorize_resource+ call.
104
+ # If neither of these authorization methods are called,
105
+ # a CanCan::AuthorizationNotPerformed exception will be raised.
106
+ # This is normally added to the ApplicationController to ensure all controller actions do authorization.
107
+ #
108
+ # class ApplicationController < ActionController::Base
109
+ # check_authorization
110
+ # end
111
+ #
112
+ # See skip_authorization_check to bypass this check on specific controller actions.
113
+ #
114
+ # Options:
115
+ # [:+only+]
116
+ # Only applies to given actions.
117
+ #
118
+ # [:+except+]
119
+ # Does not apply to given actions.
120
+ #
121
+ # [:+if+]
122
+ # Supply the name of a controller method to be called.
123
+ # The authorization check only takes place if this returns true.
124
+ #
125
+ # check_authorization :if => :admin_controller?
126
+ #
127
+ # [:+unless+]
128
+ # Supply the name of a controller method to be called.
129
+ # The authorization check only takes place if this returns false.
130
+ #
131
+ # check_authorization :unless => :devise_controller?
132
+ #
133
+ def check_authorization(**options)
134
+ after_action(**options) do |controller|
135
+ next if controller.instance_variable_defined?(:@_authorized)
136
+ raise AuthorizationNotPerformed,
137
+ 'This action failed the check_authorization because it does not authorize_resource. '\
138
+ 'Add skip_authorization_check to bypass this check.'
139
+ end
140
+ end
141
+
142
+ # Call this in the class of a controller to skip the check_authorization behavior on the actions.
143
+ #
144
+ # class HomeController < ApplicationController
145
+ # skip_authorization_check :only => :index
146
+ # end
147
+ #
148
+ # Any arguments are passed to the +before_action+ it triggers.
149
+ def skip_authorization_check(*args)
150
+ before_action(*args) do |controller|
151
+ controller.instance_variable_set(:@_authorized, true)
152
+ end
153
+ end
154
+
155
+ def cancan_skipper
156
+ @_cancan_skipper ||= { authorize: {}, load: {} }
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end