oso-oso 0.21.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a9fbbff1209cf44803895d162562cb6d9c8a4c37
4
- data.tar.gz: 8196f85296430d4c85a283bfec132c05a86e1433
3
+ metadata.gz: 719c88b2531d9c6d1e638928e6be3abba68b67d0
4
+ data.tar.gz: 327124db69d3aa57ff06a1089c24124d60175c26
5
5
  SHA512:
6
- metadata.gz: 2d49aec9759922b92b874f981709a57b6c04afc1b30762302c6020a37bde144d0b56b0343482f7f18ed96f3c3d298a0e493967fbbf8939fb91cc7dc493417e54
7
- data.tar.gz: 867123b379ef0ff554f9c433d317826e6b6e3b84a26008fe97e56f83d588737064ba71b61f4c5848969b7c6f6a0fd2cef9e8a488f238a86a152dcef7b452844e
6
+ metadata.gz: 4af7eba0a1bcac195ae51719991191ea78d4c0437bf7aacad7f09fce54d848559178485e098b5a9b150353c3feececd415533e7f311592e38cd6df8d263772c1
7
+ data.tar.gz: 57bca613ce91ddf645d7cafb89641286c1c41f1aec70cb4c9378b3157b8cd4291315e08083f19add35dad38325e423539831ffdcce2bc610bd685d328d2703e1
data/.gitignore CHANGED
@@ -7,7 +7,7 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  vendor
10
- active_record_test.db
10
+ *test.db
11
11
 
12
12
  # rspec failure tracking
13
13
  .rspec_status
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.21.0)
4
+ oso-oso (0.24.0)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
@@ -21,14 +21,14 @@ GEM
21
21
  arel (9.0.0)
22
22
  ast (2.4.2)
23
23
  backport (1.2.0)
24
- benchmark (0.1.1)
24
+ benchmark (0.2.0)
25
25
  byebug (11.1.3)
26
26
  coderay (1.1.3)
27
27
  concurrent-ruby (1.1.9)
28
28
  diff-lcs (1.4.4)
29
29
  e2mmap (0.1.0)
30
30
  ffi (1.15.4)
31
- i18n (1.8.10)
31
+ i18n (1.8.11)
32
32
  concurrent-ruby (~> 1.0)
33
33
  jaro_winkler (1.5.4)
34
34
  maruku (0.7.3)
@@ -49,7 +49,7 @@ GEM
49
49
  rainbow (3.0.0)
50
50
  rake (12.3.3)
51
51
  regexp_parser (2.1.1)
52
- reverse_markdown (2.0.0)
52
+ reverse_markdown (2.1.1)
53
53
  nokogiri
54
54
  rexml (3.2.5)
55
55
  rspec (3.10.0)
@@ -64,7 +64,7 @@ GEM
64
64
  rspec-mocks (3.10.2)
65
65
  diff-lcs (>= 1.2.0, < 2.0)
66
66
  rspec-support (~> 3.10.0)
67
- rspec-support (3.10.2)
67
+ rspec-support (3.10.3)
68
68
  rubocop (0.89.1)
69
69
  parallel (~> 1.10)
70
70
  parser (>= 2.7.1.1)
@@ -97,12 +97,13 @@ GEM
97
97
  tilt (2.0.10)
98
98
  tzinfo (1.2.9)
99
99
  thread_safe (~> 0.1)
100
- unicode-display_width (1.7.0)
100
+ unicode-display_width (1.8.0)
101
101
  yard (0.9.26)
102
102
 
103
103
  PLATFORMS
104
104
  ruby
105
105
  x86_64-darwin-20
106
+ x86_64-linux
106
107
 
107
108
  DEPENDENCIES
108
109
  activerecord
data/Makefile CHANGED
@@ -7,7 +7,7 @@ install:
7
7
  bundle install
8
8
 
9
9
  test: install rust
10
- bundle exec rake spec
10
+ POLAR_IGNORE_NO_ALLOW_WARNING=1 bundle exec rake spec
11
11
 
12
12
  lint: install
13
13
  bundle exec rubocop
Binary file
Binary file
Binary file
data/lib/oso/oso.rb CHANGED
@@ -181,27 +181,17 @@ module Oso
181
181
  # @param resource_cls The resource being accessed.
182
182
  #
183
183
  # @return A query for resources accessible to the actor.
184
- def authorized_query(actor, action, resource_cls) # rubocop:disable Metrics/MethodLength
185
- resource = Polar::Variable.new 'resource'
186
-
187
- results = query_rule(
188
- 'allow',
189
- actor,
190
- action,
191
- resource,
192
- bindings: { 'resource' => type_constraint(resource, resource_cls) },
193
- accept_expression: true
194
- )
195
-
196
- results = results.each_with_object([]) do |result, out|
197
- result.each do |key, val|
198
- out.push({ 'bindings' => { key => host.to_polar(val) } })
184
+ def authorized_query(actor, action, resource_cls)
185
+ if host.use_new_data_filtering?
186
+
187
+ unless host.types[resource_cls].build_query == ::Oso::Polar::Host::DEFAULT_BUILD_QUERY
188
+ warn 'Warning: redundant data filtering configuration detected'
199
189
  end
200
- end
201
190
 
202
- ::Oso::Polar::DataFiltering::FilterPlan
203
- .parse(self, results, get_class_name(resource_cls))
204
- .build_query
191
+ new_authorized_query(actor, action, resource_cls)
192
+ else
193
+ old_authorized_query(actor, action, resource_cls)
194
+ end
205
195
  end
206
196
 
207
197
  # Determine the resources of type +resource_cls+ that +actor+
@@ -213,10 +203,29 @@ module Oso
213
203
  #
214
204
  # @return A list of resources accessible to the actor.
215
205
  def authorized_resources(actor, action, resource_cls)
216
- q = authorized_query actor, action, resource_cls
217
- return [] if q.nil?
206
+ q = authorized_query(actor, action, resource_cls)
207
+
208
+ if host.use_new_data_filtering?
209
+ host.adapter.execute_query q
210
+ elsif q.nil?
211
+ []
212
+ else
213
+ host.types[resource_cls].exec_query[q]
214
+ end
215
+ end
216
+
217
+ # Register default values for data filtering query functions.
218
+ # These can be overridden by passing specific implementations to
219
+ # `register_class` or by defining `build_query`, `exec_query` and
220
+ # `combine_query` methods on the class object.
221
+ def set_data_filtering_query_defaults(build_query: nil, exec_query: nil, combine_query: nil)
222
+ host.build_query = build_query if build_query
223
+ host.exec_query = exec_query if exec_query
224
+ host.combine_query = combine_query if combine_query
225
+ end
218
226
 
219
- host.types[get_class_name resource_cls].exec_query[q]
227
+ def data_filtering_adapter=(adapter)
228
+ host.adapter = adapter
220
229
  end
221
230
  end
222
231
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module Data
6
+ class Adapter
7
+ # Example data filtering adapter for ActiveRecord
8
+ class ActiveRecordAdapter < Adapter
9
+ def build_query(filter) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ types = filter.types
11
+ query = filter.relations.reduce(filter.model.all) do |q, rel|
12
+ rec = types[rel.left].fields[rel.name]
13
+ q.joins(
14
+ "INNER JOIN #{rel.right.table_name} ON " \
15
+ "#{rel.left.table_name}.#{rec.my_field} = " \
16
+ "#{rel.right.table_name}.#{rec.other_field}"
17
+ )
18
+ end
19
+
20
+ filter.conditions.map do |conjs|
21
+ conjs.reduce(query) do |q, conj|
22
+ q.where(*sqlize(conj))
23
+ end
24
+ end.reduce(:or).distinct
25
+ end
26
+
27
+ def execute_query(query)
28
+ query.to_a
29
+ end
30
+
31
+ OPS = {
32
+ 'Eq' => '=', 'In' => 'IN', 'Nin' => 'NOT IN', 'Neq' => '!='
33
+ }.freeze
34
+
35
+ private
36
+
37
+ def sqlize(cond)
38
+ args = []
39
+ lhs = add_side cond.left, args
40
+ rhs = add_side cond.right, args
41
+ args.unshift "#{lhs} #{OPS[cond.cmp]} #{rhs}"
42
+ end
43
+
44
+ def add_side(side, args)
45
+ if side.is_a? ::Oso::Polar::Data::Filter::Projection
46
+ "#{side.source.table_name}.#{side.field || side.source.primary_key}"
47
+ else
48
+ args.push side
49
+ '?'
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module Data
6
+ # Abstract data adapter
7
+ #
8
+ # An Adapter has to implement two methods.
9
+ class Adapter
10
+ # Make a query object from a filter
11
+ def build_query(_filter)
12
+ raise "build_query not implemented for #{self}"
13
+ end
14
+
15
+ # Make a list of objects from a query
16
+ def execute_query(_query)
17
+ raise "execute_query not implemented for #{self}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Data filtering interface for Ruby
6
+ module Data
7
+ # Abstract data filter used by the Adapter API.
8
+ class Filter
9
+ attr_reader :model, :relations, :conditions, :types
10
+
11
+ def initialize(model:, relations:, conditions:, types:)
12
+ @model = model
13
+ @relations = relations
14
+ @conditions = conditions
15
+ @types = types
16
+ end
17
+
18
+ def self.parse(polar, blob)
19
+ types = polar.host.types
20
+ model = types[blob['root']].klass.get
21
+ relations = blob['relations'].map do |rel|
22
+ Relation.parse(polar, *rel)
23
+ end
24
+ conditions = blob['conditions'].map do |disj|
25
+ disj.map { |conj| Condition.parse(polar, *conj) }
26
+ end
27
+ new(model: model, relations: relations, conditions: conditions, types: types)
28
+ end
29
+
30
+ Projection = Struct.new(:source, :field)
31
+
32
+ Relation = Struct.new(:left, :name, :right) do
33
+ def self.parse(polar, left, name, right)
34
+ Relation.new(polar.name_to_class(left), name, polar.name_to_class(right))
35
+ end
36
+ end
37
+
38
+ Condition = Struct.new(:left, :cmp, :right) do
39
+ def self.parse(polar, left, cmp, right)
40
+ Condition.new(parse_side(polar, left), cmp, parse_side(polar, right))
41
+ end
42
+
43
+ def self.parse_side(polar, side)
44
+ key = side.keys.first
45
+ val = side[key]
46
+ case key
47
+ when 'Field'
48
+ Projection.new(polar.name_to_class(val[0]), val[1])
49
+ when 'Immediate'
50
+ polar.host.to_ruby('value' => [[val.keys.first, val.values.first]])
51
+ else
52
+ raise key
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data/adapter'
4
+ require_relative 'data/filter'
@@ -4,6 +4,7 @@ module Oso
4
4
  module Polar
5
5
  # Data filtering interface for Ruby
6
6
  module DataFiltering
7
+ GETATTR = ->(x, attr) { attr.nil? ? x : x.send(attr) }
7
8
  # Represents a set of filter sequences that should allow the host
8
9
  # to obtain the records satisfying a query.
9
10
  class FilterPlan
@@ -29,7 +30,7 @@ module Oso
29
30
  result_sets.each_with_object([]) do |rs, qb|
30
31
  rs.resolve_order.each_with_object({}) do |i, set_results|
31
32
  req = rs.requests[i]
32
- cs = req.constraints.each { |c| c.ground set_results }
33
+ cs = req.ground(set_results)
33
34
  typ = @polar.host.types[req.class_tag]
34
35
  q = typ.build_query[cs]
35
36
  if i != rs.result_id
@@ -68,6 +69,7 @@ module Oso
68
69
  attr_reader :constraints, :class_tag
69
70
 
70
71
  def self.parse(polar, parsed_json)
72
+ @polar = polar
71
73
  constraints = parsed_json['constraints'].map do |con|
72
74
  Filter.parse polar, con
73
75
  end
@@ -76,6 +78,29 @@ module Oso
76
78
  new(constraints: constraints, class_tag: class_tag)
77
79
  end
78
80
 
81
+ def ground(results) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
82
+ xrefs, rest = constraints.partition do |c|
83
+ c.value.is_a?(Ref) and !c.value.result_id.nil?
84
+ end
85
+
86
+ yrefs, nrefs = xrefs.partition { |r| %w[In Eq].include? r.kind }
87
+ [[yrefs, 'In'], [nrefs, 'Nin']].each do |refs, kind|
88
+ refs.group_by { |f| f.value.result_id }.each do |rid, fils|
89
+ if fils.length > 1
90
+ value = results[rid].map { |r| fils.map { |f| GETATTR[r, f.value.field] } }
91
+ field = fils.map(&:field)
92
+ rest.push(Filter.new(kind: kind, value: value, field: field))
93
+ else
94
+ fil = fils[0]
95
+ field = fil.value.field
96
+ value = results[rid].map { |r| field.nil? ? r : r.send(field) }
97
+ rest.push(Filter.new(kind: kind, field: fil.field, value: value))
98
+ end
99
+ end
100
+ end
101
+ rest
102
+ end
103
+
79
104
  def initialize(constraints:, class_tag:)
80
105
  @constraints = constraints
81
106
  @class_tag = class_tag
@@ -127,6 +152,7 @@ module Oso
127
152
  'Eq' => ->(a, b) { a == b },
128
153
  'In' => ->(a, b) { b.include? a },
129
154
  'Neq' => ->(a, b) { a != b },
155
+ 'Nin' => ->(a, b) { !b.include?(a) },
130
156
  'Contains' => ->(a, b) { a.include? b }
131
157
  }.freeze
132
158
 
@@ -138,8 +164,6 @@ module Oso
138
164
  @kind = kind
139
165
  @field = field
140
166
  @value = value
141
- @check = CHECKS[kind]
142
- raise "Unknown constraint kind `#{kind}`" if @check.nil?
143
167
  end
144
168
 
145
169
  def ground(results)
@@ -150,10 +174,16 @@ module Oso
150
174
  @value = value.map { |v| v.send ref.field } unless ref.field.nil?
151
175
  end
152
176
 
153
- def check(item)
177
+ def check(item) # rubocop:disable Metrics/AbcSize
154
178
  val = value.is_a?(Field) ? item.send(value.field) : value
155
- item = field.nil? ? item : item.send(field)
156
- @check[item, val]
179
+ item = if field.nil?
180
+ item
181
+ elsif field.is_a? Array
182
+ field.map { |f| GETATTR[item, f] }
183
+ else
184
+ item.send field
185
+ end
186
+ CHECKS[@kind][item, val]
157
187
  end
158
188
 
159
189
  def self.parse(polar, constraint) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -23,11 +23,9 @@ module Oso
23
23
 
24
24
  # Errors from across the FFI boundary.
25
25
 
26
- class SerializationError < PolarRuntimeError; end
27
26
  class UnsupportedError < PolarRuntimeError; end
28
27
  class PolarTypeError < PolarRuntimeError; end
29
28
  class StackOverflowError < PolarRuntimeError; end
30
- class FileLoadingError < PolarRuntimeError; end
31
29
 
32
30
  # Errors originating from this side of the FFI boundary.
33
31
 
@@ -96,10 +94,6 @@ module Oso
96
94
  class UnrecognizedToken < ParseError; end
97
95
  end
98
96
 
99
- # Generic Polar API exception.
100
- class ApiError < Error; end
101
- class ParameterError < ApiError; end
102
-
103
97
  class ValidationError < Error; end
104
98
 
105
99
  UNEXPECTED_EXPRESSION_MESSAGE = <<~MSG
@@ -4,7 +4,7 @@ module Oso
4
4
  module Polar
5
5
  # Polar expression.
6
6
  class Expression
7
- attr_reader :operator, :args
7
+ attr_accessor :operator, :args
8
8
 
9
9
  # @param operator [String]
10
10
  # @param args [Array<Object>]
@@ -6,29 +6,13 @@ module Oso
6
6
  module Polar
7
7
  module FFI
8
8
  # Wrapper class for Error FFI pointer + operations.
9
- class Error < ::FFI::AutoPointer
10
- def to_s
11
- @to_s ||= read_string.force_encoding('UTF-8')
12
- end
13
-
14
- Rust = Module.new do
15
- extend ::FFI::Library
16
- ffi_lib FFI::LIB_PATH
17
-
18
- attach_function :get, :polar_get_error, [], Error
19
- attach_function :free, :string_free, [Error], :int32
20
- end
21
- private_constant :Rust
22
-
9
+ class Error
23
10
  # Check for an FFI error and convert it into a Ruby exception.
24
11
  #
25
12
  # @return [::Oso::Polar::Error] if there's an FFI error.
26
13
  # @return [::Oso::Polar::FFIErrorNotFound] if there isn't one.
27
- def self.get(enrich_message) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
- error = Rust.get
29
- return ::Oso::Polar::FFIErrorNotFound if error.null?
30
-
31
- error = JSON.parse(error.to_s)
14
+ def self.get(error_str, enrich_message) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
15
+ error = JSON.parse(error_str.to_s)
32
16
  msg = error['formatted']
33
17
  kind, body = error['kind'].first
34
18
 
@@ -54,8 +38,6 @@ module Oso
54
38
  runtime_error(subkind, msg: msg, details: details)
55
39
  when 'Operational'
56
40
  operational_error(subkind, msg: msg, details: details)
57
- when 'Parameter'
58
- api_error(subkind, msg: msg, details: details)
59
41
  when 'Validation'
60
42
  validation_error(msg, details: details)
61
43
  end
@@ -92,18 +74,14 @@ module Oso
92
74
  # @param msg [String]
93
75
  # @param details [Hash<String, Object>]
94
76
  # @return [::Oso::Polar::PolarRuntimeError] the object converted into the expected format.
95
- private_class_method def self.runtime_error(kind, msg:, details:) # rubocop:disable Metrics/MethodLength
77
+ private_class_method def self.runtime_error(kind, msg:, details:)
96
78
  case kind
97
- when 'Serialization'
98
- ::Oso::Polar::SerializationError.new(msg, details: details)
99
79
  when 'Unsupported'
100
80
  ::Oso::Polar::UnsupportedError.new(msg, details: details)
101
81
  when 'TypeError'
102
82
  ::Oso::Polar::PolarTypeError.new(msg, details: details)
103
83
  when 'StackOverflow'
104
84
  ::Oso::Polar::StackOverflowError.new(msg, details: details)
105
- when 'FileLoading'
106
- ::Oso::Polar::FileLoadingError.new(msg, details: details)
107
85
  else
108
86
  ::Oso::Polar::PolarRuntimeError.new(msg, details: details)
109
87
  end
@@ -124,21 +102,6 @@ module Oso
124
102
  end
125
103
  end
126
104
 
127
- # Map FFI API errors into Ruby exceptions.
128
- #
129
- # @param kind [String]
130
- # @param msg [String]
131
- # @param details [Hash<String, Object>]
132
- # @return [::Oso::Polar::ApiError] the object converted into the expected format.
133
- private_class_method def self.api_error(kind, msg:, details:)
134
- case kind
135
- when 'Parameter'
136
- ::Oso::Polar::ParameterError.new(msg, details: details)
137
- else
138
- ::Oso::Polar::ApiError.new(msg, details: details)
139
- end
140
- end
141
-
142
105
  # Map FFI Validation errors into Ruby exceptions.
143
106
  #
144
107
  # @param msg [String]