dry-ability 0.0.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.
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