oso-oso 0.23.0 → 0.26.0

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 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