policier 0.1.0

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: 4f772e41751bc6c685d9f0eb2bfa2690979e58657cdc81b596f2be976d9610c9
4
+ data.tar.gz: 2eea37673f5297331a41eacc79ad3279de900a3e359fef5d4f0debcf81e4edbf
5
+ SHA512:
6
+ metadata.gz: 8e163c80ec8a5a9257b5ca86291a1891c43e1aac5f6ce49eea3aa7264d67eb3e1ab8c33a18a8dab8ce832628bd88241029b6c18bb8c1b4cc23f14663023e54c8
7
+ data.tar.gz: 747fbf5ae7fe59199397531fafea6a62641bb1307013ae798b72e7d512a2f08a20edd2c0d6162ac9e1c3a14f397d641d8dc3f60a9f74ba83e3158f4f1ff5e6b4
data/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ [*.{rb,rake}]
2
+ charset = utf-8
3
+ end_of_line = lf
4
+ indent_style = space
5
+ indent_size = 2
data/.rubocop.yml ADDED
@@ -0,0 +1,27 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
9
+
10
+ Layout/FirstArrayElementIndentation:
11
+ EnforcedStyle: consistent
12
+
13
+ Layout/FirstHashElementIndentation:
14
+ EnforcedStyle: consistent
15
+
16
+ Style/Documentation:
17
+ Enabled: false
18
+
19
+ Metrics/MethodLength:
20
+ Enabled: false
21
+
22
+ Metrics/AbcSize:
23
+ Enabled: false
24
+
25
+ Metrics/BlockLength:
26
+ Exclude:
27
+ - test/**/*
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
@@ -0,0 +1,6 @@
1
+ {
2
+ "[ruby]": {
3
+ "editor.defaultFormatter": "misogi.ruby-rubocop",
4
+ "editor.formatOnSave": false
5
+ },
6
+ }
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Policier
2
+
3
+ A gem to build ACL policies with style. Comparing to Pundit and Cancan, the gem tries to focus on DSL and structure to enforce writing ACLS in the way you can read them afterwards.
4
+ Policier consists of two big sections:
5
+
6
+ ## Conditions
7
+
8
+ ```ruby
9
+ # Activates whe current user is super
10
+ class SuperuserCondition < Policier::Condition
11
+ self.data = Struct.new(:authorized_at)
12
+
13
+ # This is the main check, it's happenuing always when any policy
14
+ # is applied against the context (from controller or GraphQL)
15
+ #
16
+ # If it's veritied, condition is activated and causes extension
17
+ # of access rights (see below)
18
+ verify_with do
19
+ fail! if payload[:user].blank?
20
+ fail! unless payload[:user].is_superadmin
21
+
22
+ # This is not very useful since data is located next to payload
23
+ # so this is only here as an example
24
+ data[:authorized_at] = context[:user].authorized_at
25
+ end
26
+
27
+ # Additional check that can be quuickly used on top of verified
28
+ # condition to make conditions anagement more flexible.
29
+ # Think of it as of a Trait in FactoryBot
30
+ also_ensure(:it_wasnt_thursday) do |important_date|
31
+ fail! if important_date.wday == 3
32
+ end
33
+
34
+ # You can nhave as many as you want
35
+ also_ensure(:it_was_thursday) do |important_date|
36
+ fail! unless important_date.wday == 3
37
+ end
38
+ end
39
+ ```
40
+
41
+ ## Policies
42
+
43
+ ```ruby
44
+ # Creates a dynamic scope over Person model that starts withb Person.none
45
+ # and extends when conditions activate
46
+
47
+ class PersonPolicy < Policier::Policy
48
+ scope(Person) do
49
+ # Collector argument allows you to propagate values you had during
50
+ # condition verification into actual policy
51
+ allow SuperuserCondition.and_it_wasnt_thursday(2.weeks.ago)
52
+ scope where(id: 5000)
53
+ scope where(id: 6000)
54
+ end
55
+
56
+ # This syntax allows you to combine several conditions and it runs
57
+ # if any of them activated for eahc of them
58
+ allow SuperuserCondition | AnotherCnndition
59
+ scope where('id < 1000')
60
+ end
61
+
62
+ # Thirsdays are the best
63
+ allow SuperuserCondition.and_it_was_thursday(2.weeks.ago)
64
+ scope all
65
+ end
66
+ end
67
+ end
68
+ ```
69
+
70
+ As the outcome of this policy, if no conditions activate, `Person.count`
71
+ will be 0. And the every activated condition triggers `to` that get merged
72
+ and you get access to all of parts of relational scope.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+
5
+ require_relative "condition_union"
6
+
7
+ module Policier
8
+ module ConditionResolve
9
+ def resolve
10
+ Context.current.ensure_condiiton(self)
11
+ end
12
+
13
+ def union
14
+ resolve.union
15
+ end
16
+
17
+ def |(other)
18
+ union | other
19
+ end
20
+ end
21
+
22
+ class Condition
23
+ class FailedException < StandardError; end
24
+
25
+ extend ConditionResolve
26
+
27
+ class << self
28
+ attr_accessor :data_class
29
+ end
30
+
31
+ attr_reader :data
32
+
33
+ def initialize
34
+ @context = Context.current
35
+ @data = @context.init_data(self.class, self.class.data_class) if self.class.data_class.present?
36
+ @failed = false
37
+ @executed = false
38
+ end
39
+
40
+ def depend_on!(condition_klass)
41
+ condition = condition_klass.resolve
42
+ return fail! if condition.failed?
43
+
44
+ condition.data
45
+ end
46
+
47
+ def fail!
48
+ raise FailedException
49
+ end
50
+
51
+ def payload
52
+ @context.payload
53
+ end
54
+
55
+ def failed?
56
+ @failed
57
+ end
58
+
59
+ def verify
60
+ return self if @executed
61
+
62
+ @failed ||= !instance_exec_with_failures(&self.class.verification_block)
63
+ @executed = true
64
+ self
65
+ end
66
+
67
+ def override!(failing: false, data_replacement: {})
68
+ @failed = failing
69
+ @executed = true
70
+ data_replacement.each { |k, v| data[k] = v }
71
+ self
72
+ end
73
+
74
+ def instance_exec_with_failures(*args, &block)
75
+ instance_exec(*args, &block)
76
+ true
77
+ rescue FailedException
78
+ false
79
+ end
80
+
81
+ def union
82
+ ConditionUnion.new(self)
83
+ end
84
+
85
+ class << self
86
+ attr_reader :verification_block
87
+ attr_accessor :collector
88
+
89
+ def verify_with(&block)
90
+ @verification_block = block
91
+ end
92
+
93
+ def also_ensure(name, &block)
94
+ define_method :"and_#{name}" do |data|
95
+ @failed ||= !instance_exec_with_failures(data, &block)
96
+ self
97
+ end
98
+ end
99
+
100
+ def handle
101
+ Dry::Inflector.new.underscore(name).gsub("/", "_").to_sym
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Policier
4
+ class ConditionUnion
5
+ attr_reader :conditions, :context
6
+
7
+ def initialize(*conditions)
8
+ @context = Context.current
9
+ @conditions = []
10
+ conditions.each { |c| self | c }
11
+ end
12
+
13
+ def |(other)
14
+ other = other.resolve if other.is_a?(Class)
15
+ @conditions << other
16
+ self
17
+ end
18
+
19
+ def union
20
+ self
21
+ end
22
+
23
+ def payload
24
+ @context.payload
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+
5
+ require_relative "condition_union"
6
+
7
+ module Policier
8
+ class Context
9
+ class NotInScopeException < StandardError; end
10
+ class ScopeStartedEvaluation < StandardError; end
11
+ class DuplicatePayloadKey < StandardError; end
12
+
13
+ THREAD_CURRENT_KEY = :policier_context_current
14
+
15
+ class << self
16
+ def current
17
+ raise NotInScopeException unless Thread.current[THREAD_CURRENT_KEY]
18
+
19
+ Thread.current[THREAD_CURRENT_KEY]
20
+ end
21
+
22
+ def scope(payload = {})
23
+ if Thread.current[THREAD_CURRENT_KEY].present? && Context.current.evaluation_started?
24
+ raise ScopeStartedEvaluation
25
+ end
26
+
27
+ if Thread.current[THREAD_CURRENT_KEY].blank?
28
+ Thread.current[THREAD_CURRENT_KEY] = new(payload)
29
+ else
30
+ raise ScopeStartedEvaluation if Context.current.evaluation_started? && payload.any?
31
+
32
+ Context.current.payload.merge!(payload)
33
+ end
34
+
35
+ yield
36
+ ensure
37
+ Thread.current[THREAD_CURRENT_KEY] = nil
38
+ end
39
+ end
40
+
41
+ attr_reader :payload
42
+
43
+ def initialize(payload)
44
+ @payload = payload
45
+ @conditions = {}
46
+ @data = {}
47
+ end
48
+
49
+ def init_data(condition_class, data_class)
50
+ @data[condition_class] = data_class.new
51
+ end
52
+
53
+ def mock_condition(*condtion_classes, failed: false, data_replacement: {})
54
+ condtion_classes.each do |condition_class|
55
+ @condition[condition_class] ||= condition_class.new.iverride!(
56
+ failed: failed,
57
+ data_replacement: data_replacement
58
+ )
59
+ end
60
+ end
61
+
62
+ def evaluation_started?
63
+ @conditions.present?
64
+ end
65
+
66
+ def ensure_condiiton(condition_class)
67
+ @conditions[condition_class] ||= condition_class.new.verify
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "runner"
4
+
5
+ module Policier
6
+ class Policy
7
+ extend Runner::DSL
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+
5
+ require_relative "condition_union"
6
+
7
+ module Policier
8
+ class Runner
9
+ module DSL
10
+ def self.extended(_base)
11
+ attr_reader :model
12
+ end
13
+
14
+ def scope(model, &block)
15
+ @model = model
16
+ @scope = block
17
+ end
18
+
19
+ def run
20
+ runner = Runner.new(self)
21
+ runner.instance_eval(&@scope)
22
+ runner.scope_union
23
+ end
24
+ end
25
+
26
+ attr_reader :scope_union
27
+
28
+ def initialize(policy)
29
+ @policy = policy
30
+ @scope_union = ScopeUnion.new(policy.model)
31
+ end
32
+
33
+ def allow(condition_union, &block)
34
+ condition_union.union.conditions.each do |condition|
35
+ next if condition.failed?
36
+
37
+ @scope_union.instance_exec(condition, &block)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Policier
4
+ class ScopeUnion
5
+ attr_reader :relation
6
+
7
+ def initialize(model = nil)
8
+ @context = Context.current
9
+ @model = model
10
+ @relation = model.none if model.present?
11
+ @visible = false
12
+ @allowed_methods = Set.new
13
+ end
14
+
15
+ def can?(method)
16
+ @allowed_methods.include?(method)
17
+ end
18
+
19
+ def visible?
20
+ @visible
21
+ end
22
+
23
+ def view
24
+ @visible = true
25
+ end
26
+
27
+ def scope(update)
28
+ @relation = @relation.or(update)
29
+ end
30
+
31
+ def exec(method)
32
+ @allowed_methods.add(method)
33
+ end
34
+
35
+ def payload
36
+ @context.payload
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Policier
4
+ VERSION = "0.1.0"
5
+ end
data/lib/policier.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ # frozen_string_literal: tue
3
+
4
+ require_relative "policier/version"
5
+ require_relative "policier/context"
6
+ require_relative "policier/policy"
7
+ require_relative "policier/runner"
8
+ require_relative "policier/condition"
9
+ require_relative "policier/condition_union"
10
+ require_relative "policier/scope_union"
11
+
12
+ module Policier
13
+ class Error < StandardError; end
14
+ # Your code goes here...
15
+ end
data/sig/policier.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Policier
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: policier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Boris Staal
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-04-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-inflector
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ description:
28
+ email:
29
+ - boris@staal.io
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".editorconfig"
35
+ - ".rubocop.yml"
36
+ - ".ruby-version"
37
+ - ".vscode/settings.json"
38
+ - README.md
39
+ - Rakefile
40
+ - lib/policier.rb
41
+ - lib/policier/condition.rb
42
+ - lib/policier/condition_union.rb
43
+ - lib/policier/context.rb
44
+ - lib/policier/policy.rb
45
+ - lib/policier/runner.rb
46
+ - lib/policier/scope_union.rb
47
+ - lib/policier/version.rb
48
+ - sig/policier.rbs
49
+ homepage: https://github.com/inossidabile/policier
50
+ licenses: []
51
+ metadata:
52
+ homepage_uri: https://github.com/inossidabile/policier
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.5.3
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: The policier
72
+ test_files: []