oso-oso 0.23.0 → 0.26.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: 1e323fdc380d0763cb3b4d84618fb81c3b6957a8
4
- data.tar.gz: 8571c1594862ec12dd1ed9bbbc49dec49a71567b
3
+ metadata.gz: 9c6a70f052d3142dbaf1f8d9c699a7f3c6697060
4
+ data.tar.gz: 9820ca345e2b1bb85c31183a3f43a1748d412d77
5
5
  SHA512:
6
- metadata.gz: 451a25cd51d463002223839c152a96aed018b958b0619103c8a7ad2e7e31fe4bab05ffded087ecde46036849b07d5f7ab34b5e8a5b2f6d761a741fe255c817a6
7
- data.tar.gz: 2ccbcd09c12aeeedc86ce473b8f76439bc576016fb087dbbf9b8279c7a14128847130492698c37bdc76be2da5792fc674ee2be384e12f6f787c738fa5539cc6e
6
+ metadata.gz: '08cabc94530a44164fef27de25cac7477fc8906b6a4766197b003c4f629332ce4a24b88fc96e7fa78e305c23f0e16f0f5df5481bc7ffb82e5c51a9ffb7cd260c'
7
+ data.tar.gz: f534eec40e00a8c10dc9379843e8b08dc4bc95bd6526e4c00e0a88aaca11877b31bf643d82e934fe0b01ccdb1d957ae25c3fa069875e46ea7765634313bd43ec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.23.0)
4
+ oso-oso (0.26.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,8 @@ 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) } })
199
- end
200
- end
201
-
202
- ::Oso::Polar::DataFiltering::FilterPlan
203
- .parse(self, results, get_class_name(resource_cls))
204
- .build_query
184
+ def authorized_query(actor, action, resource_cls)
185
+ new_authorized_query(actor, action, resource_cls)
205
186
  end
206
187
 
207
188
  # Determine the resources of type +resource_cls+ that +actor+
@@ -213,20 +194,11 @@ module Oso
213
194
  #
214
195
  # @return A list of resources accessible to the actor.
215
196
  def authorized_resources(actor, action, resource_cls)
216
- q = authorized_query actor, action, resource_cls
217
- return [] if q.nil?
218
-
219
- host.types[get_class_name resource_cls].exec_query[q]
197
+ host.adapter.execute_query authorized_query(actor, action, resource_cls)
220
198
  end
221
199
 
222
- # Register default values for data filtering query functions.
223
- # These can be overridden by passing specific implementations to
224
- # `register_class` or by defining `build_query`, `exec_query` and
225
- # `combine_query` methods on the class object.
226
- def set_data_filtering_query_defaults(build_query: nil, exec_query: nil, combine_query: nil)
227
- host.build_query = build_query if build_query
228
- host.exec_query = exec_query if exec_query
229
- host.combine_query = combine_query if combine_query
200
+ def data_filtering_adapter=(adapter)
201
+ host.adapter = adapter
230
202
  end
231
203
  end
232
204
  end
@@ -0,0 +1,57 @@
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
+ 'Lt' => '<', 'Gt' => '>', 'Leq' => '<=', 'Geq' => '>='
34
+ }.freeze
35
+
36
+ private
37
+
38
+ def sqlize(cond)
39
+ args = []
40
+ lhs = add_side cond.left, args
41
+ rhs = add_side cond.right, args
42
+ args.unshift "#{lhs} #{OPS[cond.cmp]} #{rhs}"
43
+ end
44
+
45
+ def add_side(side, args)
46
+ if side.is_a? ::Oso::Polar::Data::Filter::Projection
47
+ "#{side.source.table_name}.#{side.field || side.source.primary_key}"
48
+ else
49
+ args.push side
50
+ '?'
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ 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,110 +4,6 @@ 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) }
8
- # Represents a set of filter sequences that should allow the host
9
- # to obtain the records satisfying a query.
10
- class FilterPlan
11
- attr_reader :result_sets
12
-
13
- def self.parse(polar, partials, class_name)
14
- types = polar.host.serialize_types
15
- parsed_json = polar.ffi.build_filter_plan(types, partials, 'resource', class_name)
16
- result_sets = parsed_json['result_sets'].map do |rset|
17
- ResultSet.parse polar, rset
18
- end
19
-
20
- new polar: polar, result_sets: result_sets
21
- end
22
-
23
- def initialize(polar:, result_sets:)
24
- @polar = polar
25
- @result_sets = result_sets
26
- end
27
-
28
- def build_query # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
29
- combine = nil
30
- result_sets.each_with_object([]) do |rs, qb|
31
- rs.resolve_order.each_with_object({}) do |i, set_results|
32
- req = rs.requests[i]
33
- cs = req.ground(set_results)
34
- typ = @polar.host.types[req.class_tag]
35
- q = typ.build_query[cs]
36
- if i != rs.result_id
37
- set_results[i] = typ.exec_query[q]
38
- else
39
- combine = typ.combine_query
40
- qb.push q
41
- end
42
- end
43
- end.reduce(&combine)
44
- end
45
-
46
- # Represents a sequence of filters for one set of results
47
- class ResultSet
48
- attr_reader :requests, :resolve_order, :result_id
49
-
50
- def self.parse(polar, parsed_json)
51
- resolve_order = parsed_json['resolve_order']
52
- result_id = parsed_json['result_id']
53
- requests = parsed_json['requests'].each_with_object({}) do |req, reqs|
54
- reqs[req[0].to_i] = Request.parse(polar, req[1])
55
- end
56
-
57
- new resolve_order: resolve_order, result_id: result_id, requests: requests
58
- end
59
-
60
- def initialize(requests:, resolve_order:, result_id:)
61
- @resolve_order = resolve_order
62
- @requests = requests
63
- @result_id = result_id
64
- end
65
- end
66
-
67
- # Represents a filter for a result set
68
- class Request
69
- attr_reader :constraints, :class_tag
70
-
71
- def self.parse(polar, parsed_json)
72
- @polar = polar
73
- constraints = parsed_json['constraints'].map do |con|
74
- Filter.parse polar, con
75
- end
76
- class_tag = parsed_json['class_tag']
77
-
78
- new(constraints: constraints, class_tag: class_tag)
79
- end
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
-
104
- def initialize(constraints:, class_tag:)
105
- @constraints = constraints
106
- @class_tag = class_tag
107
- end
108
- end
109
- end
110
-
111
7
  # Represents relationships between resources, eg. one-one or one-many
112
8
  class Relation
113
9
  attr_reader :kind, :other_type, :my_field, :other_field
@@ -124,92 +20,6 @@ module Oso
124
20
  @other_field = other_field
125
21
  end
126
22
  end
127
-
128
- # Represents field-field relationships on one resource.
129
- class Field
130
- attr_reader :field
131
-
132
- def initialize(field:)
133
- @field = field
134
- end
135
- end
136
-
137
- # Represents field-field relationships on different resources.
138
- class Ref
139
- attr_reader :field, :result_id
140
-
141
- def initialize(field:, result_id:)
142
- @field = field
143
- @result_id = result_id
144
- end
145
- end
146
-
147
- # Represents a condition that must hold on a resource.
148
- class Filter
149
- attr_reader :kind, :field, :value
150
-
151
- CHECKS = {
152
- 'Eq' => ->(a, b) { a == b },
153
- 'In' => ->(a, b) { b.include? a },
154
- 'Neq' => ->(a, b) { a != b },
155
- 'Nin' => ->(a, b) { !b.include?(a) },
156
- 'Contains' => ->(a, b) { a.include? b }
157
- }.freeze
158
-
159
- # Create a new predicate for data filtering.
160
- # @param kind [String] Represents a condition. One of "Eq", "Neq", "In", "Contains".
161
- # @param field The field the condition applies to.
162
- # @param value The value with which to compare the field according to the condition.
163
- def initialize(kind:, field:, value:)
164
- @kind = kind
165
- @field = field
166
- @value = value
167
- end
168
-
169
- def ground(results)
170
- return unless value.is_a? Ref
171
-
172
- ref = value
173
- @value = results[ref.result_id]
174
- @value = value.map { |v| v.send ref.field } unless ref.field.nil?
175
- end
176
-
177
- def check(item) # rubocop:disable Metrics/AbcSize
178
- val = value.is_a?(Field) ? item.send(value.field) : value
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]
187
- end
188
-
189
- def self.parse(polar, constraint) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
190
- kind = constraint['kind']
191
- field = constraint['field']
192
- value = constraint['value']
193
-
194
- value_kind = value.keys.first
195
- value = value[value_kind]
196
-
197
- case value_kind
198
- when 'Term'
199
- value = polar.host.to_ruby value
200
- when 'Ref'
201
- child_field = value['field']
202
- result_id = value['result_id']
203
- value = Ref.new field: child_field, result_id: result_id
204
- when 'Field'
205
- value = Field.new field: value
206
- else
207
- raise "Unknown value kind `#{value_kind}`"
208
- end
209
-
210
- new kind: kind, field: field, value: value
211
- end
212
- end
213
23
  end
214
24
  end
215
25
  end
@@ -26,7 +26,6 @@ module Oso
26
26
  class UnsupportedError < PolarRuntimeError; end
27
27
  class PolarTypeError < PolarRuntimeError; end
28
28
  class StackOverflowError < PolarRuntimeError; end
29
- class FileLoadingError < PolarRuntimeError; end
30
29
 
31
30
  # Errors originating from this side of the FFI boundary.
32
31
 
@@ -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
 
@@ -90,7 +74,7 @@ module Oso
90
74
  # @param msg [String]
91
75
  # @param details [Hash<String, Object>]
92
76
  # @return [::Oso::Polar::PolarRuntimeError] the object converted into the expected format.
93
- 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:)
94
78
  case kind
95
79
  when 'Unsupported'
96
80
  ::Oso::Polar::UnsupportedError.new(msg, details: details)
@@ -98,8 +82,6 @@ module Oso
98
82
  ::Oso::Polar::PolarTypeError.new(msg, details: details)
99
83
  when 'StackOverflow'
100
84
  ::Oso::Polar::StackOverflowError.new(msg, details: details)
101
- when 'FileLoading'
102
- ::Oso::Polar::FileLoadingError.new(msg, details: details)
103
85
  else
104
86
  ::Oso::Polar::PolarRuntimeError.new(msg, details: details)
105
87
  end