oso-oso 0.23.0 → 0.24.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: 719c88b2531d9c6d1e638928e6be3abba68b67d0
4
+ data.tar.gz: 327124db69d3aa57ff06a1089c24124d60175c26
5
5
  SHA512:
6
- metadata.gz: 451a25cd51d463002223839c152a96aed018b958b0619103c8a7ad2e7e31fe4bab05ffded087ecde46036849b07d5f7ab34b5e8a5b2f6d761a741fe255c817a6
7
- data.tar.gz: 2ccbcd09c12aeeedc86ce473b8f76439bc576016fb087dbbf9b8279c7a14128847130492698c37bdc76be2da5792fc674ee2be384e12f6f787c738fa5539cc6e
6
+ metadata.gz: 4af7eba0a1bcac195ae51719991191ea78d4c0437bf7aacad7f09fce54d848559178485e098b5a9b150353c3feececd415533e7f311592e38cd6df8d263772c1
7
+ data.tar.gz: 57bca613ce91ddf645d7cafb89641286c1c41f1aec70cb4c9378b3157b8cd4291315e08083f19add35dad38325e423539831ffdcce2bc610bd685d328d2703e1
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.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,15 @@ 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?
218
-
219
- host.types[get_class_name resource_cls].exec_query[q]
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
220
215
  end
221
216
 
222
217
  # Register default values for data filtering query functions.
@@ -228,5 +223,9 @@ module Oso
228
223
  host.exec_query = exec_query if exec_query
229
224
  host.combine_query = combine_query if combine_query
230
225
  end
226
+
227
+ def data_filtering_adapter=(adapter)
228
+ host.adapter = adapter
229
+ end
231
230
  end
232
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'
@@ -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
@@ -6,7 +6,7 @@ module Oso
6
6
  module Polar
7
7
  module FFI
8
8
  # Wrapper class for Polar FFI pointer + operations.
9
- class Polar < ::FFI::AutoPointer
9
+ class Polar < ::FFI::AutoPointer # rubocop:disable Metrics/ClassLength
10
10
  attr_accessor :enrich_message
11
11
 
12
12
  Rust = Module.new do
@@ -14,21 +14,28 @@ module Oso
14
14
  ffi_lib FFI::LIB_PATH
15
15
 
16
16
  attach_function :new, :polar_new, [], FFI::Polar
17
- attach_function :load, :polar_load, [FFI::Polar, :string], :int32
18
- attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
17
+ attach_function :load, :polar_load, [FFI::Polar, :string], CResultVoid
18
+ attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], CResultVoid
19
19
  attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
20
20
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
21
- attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
22
- attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], FFI::Query
23
- attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
24
- attach_function :register_mro, :polar_register_mro, [FFI::Polar, :string, :string], :int32
25
- attach_function :next_message, :polar_next_polar_message, [FFI::Polar], FFI::Message
21
+ attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], CResultQuery
22
+ attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], CResultQuery
23
+ attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], CResultVoid
24
+ attach_function :register_mro, :polar_register_mro, [FFI::Polar, :string, :string], CResultVoid
25
+ attach_function :next_message, :polar_next_polar_message, [FFI::Polar], CResultString
26
26
  attach_function :free, :polar_free, [FFI::Polar], :int32
27
+ attach_function :result_free, :result_free, [:pointer], :int32
27
28
  attach_function(
28
29
  :build_filter_plan,
29
30
  :polar_build_filter_plan,
30
31
  [FFI::Polar, :string, :string, :string, :string],
31
- :string
32
+ CResultString
33
+ )
34
+ attach_function(
35
+ :build_data_filter,
36
+ :polar_build_data_filter,
37
+ [FFI::Polar, :string, :string, :string, :string],
38
+ CResultString
32
39
  )
33
40
  end
34
41
  private_constant :Rust
@@ -36,10 +43,7 @@ module Oso
36
43
  # @return [FFI::Polar]
37
44
  # @raise [FFI::Error] if the FFI call returns an error.
38
45
  def self.create
39
- polar = Rust.new
40
- handle_error if polar.null?
41
-
42
- polar
46
+ Rust.new
43
47
  end
44
48
 
45
49
  def build_filter_plan(types, partials, variable, class_tag)
@@ -47,9 +51,19 @@ module Oso
47
51
  partials = JSON.dump(partials)
48
52
  plan = Rust.build_filter_plan(self, types, partials, variable, class_tag)
49
53
  process_messages
50
- handle_error if plan.nil?
54
+ plan = check_result plan
55
+ # TODO(gw) more error checking?
56
+ JSON.parse plan.to_s
57
+ end
58
+
59
+ def build_data_filter(types, partials, variable, class_tag)
60
+ types = JSON.dump(types)
61
+ partials = JSON.dump(partials)
62
+ plan = Rust.build_data_filter(self, types, partials, variable, class_tag)
63
+ process_messages
64
+ plan = check_result plan
51
65
  # TODO(gw) more error checking?
52
- JSON.parse plan
66
+ JSON.parse plan.to_s
53
67
  end
54
68
 
55
69
  # @param sources [Array<Source>]
@@ -57,14 +71,14 @@ module Oso
57
71
  def load(sources)
58
72
  loaded = Rust.load(self, JSON.dump(sources))
59
73
  process_messages
60
- handle_error if loaded.zero?
74
+ check_result loaded
61
75
  end
62
76
 
63
77
  # @raise [FFI::Error] if the FFI call returns an error.
64
78
  def clear_rules
65
79
  cleared = Rust.clear_rules(self)
66
80
  process_messages
67
- handle_error if cleared.zero?
81
+ check_result cleared
68
82
  end
69
83
 
70
84
  # @return [FFI::Query] if there are remaining inline queries.
@@ -79,12 +93,7 @@ module Oso
79
93
  # @return [Integer]
80
94
  # @raise [FFI::Error] if the FFI call returns an error.
81
95
  def new_id
82
- id = Rust.new_id(self)
83
- # TODO(gj): I don't think this error check is correct. If getting a new ID fails on the
84
- # Rust side, it'll probably surface as a panic (e.g., the KB lock is poisoned).
85
- handle_error if id.zero?
86
-
87
- id
96
+ Rust.new_id(self)
88
97
  end
89
98
 
90
99
  # @param str [String] Query string.
@@ -93,9 +102,7 @@ module Oso
93
102
  def new_query_from_str(str)
94
103
  query = Rust.new_query_from_str(self, str, 0)
95
104
  process_messages
96
- handle_error if query.null?
97
-
98
- query
105
+ check_result query
99
106
  end
100
107
 
101
108
  # @param term [Hash<String, Object>]
@@ -104,9 +111,7 @@ module Oso
104
111
  def new_query_from_term(term)
105
112
  query = Rust.new_query_from_term(self, JSON.dump(term), 0)
106
113
  process_messages
107
- handle_error if query.null?
108
-
109
- query
114
+ check_result query
110
115
  end
111
116
 
112
117
  # @param name [String]
@@ -114,7 +119,7 @@ module Oso
114
119
  # @raise [FFI::Error] if the FFI call returns an error.
115
120
  def register_constant(value, name:)
116
121
  registered = Rust.register_constant(self, name, JSON.dump(value))
117
- handle_error if registered.zero?
122
+ check_result registered
118
123
  end
119
124
 
120
125
  # @param name [String]
@@ -122,11 +127,24 @@ module Oso
122
127
  # @raise [FFI::Error] if the FFI call returns an error.
123
128
  def register_mro(name, mro)
124
129
  registered = Rust.register_mro(self, name, JSON.dump(mro))
125
- handle_error if registered.zero?
130
+ check_result registered
126
131
  end
127
132
 
128
133
  def next_message
129
- Rust.next_message(self)
134
+ check_result Rust.next_message(self)
135
+ end
136
+
137
+ def process_message(message, enrich_message)
138
+ message = JSON.parse(message.to_s)
139
+ kind = message['kind']
140
+ msg = enrich_message.call(message['msg'])
141
+
142
+ case kind
143
+ when 'Print'
144
+ puts(msg)
145
+ when 'Warning'
146
+ warn(format('[warning] %<msg>s', msg: msg))
147
+ end
130
148
  end
131
149
 
132
150
  def process_messages
@@ -134,12 +152,19 @@ module Oso
134
152
  message = next_message
135
153
  break if message.null?
136
154
 
137
- message.process(enrich_message)
155
+ process_message(message, enrich_message)
138
156
  end
139
157
  end
140
158
 
141
- def handle_error
142
- raise FFI::Error.get(enrich_message)
159
+ def check_result(res)
160
+ result = res[:result]
161
+ error = res[:error]
162
+ Rust.result_free(res)
163
+
164
+ raise 'internal error: both result and error pointers are not null' if !error.null? && !result.zero?
165
+ raise FFI::Error.get(error, enrich_message) unless error.null?
166
+
167
+ result
143
168
  end
144
169
  end
145
170
  end
@@ -13,15 +13,16 @@ module Oso
13
13
  extend ::FFI::Library
14
14
  ffi_lib FFI::LIB_PATH
15
15
 
16
- attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], :int32
17
- attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], :int32
18
- attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], :int32
19
- attach_function :application_error, :polar_application_error, [FFI::Query, :string], :int32
20
- attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
21
- attach_function :next_message, :polar_next_query_message, [FFI::Query], FFI::Message
22
- attach_function :source, :polar_query_source_info, [FFI::Query], FFI::Source
16
+ attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], CResultVoid
17
+ attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], CResultVoid
18
+ attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], CResultVoid
19
+ attach_function :application_error, :polar_application_error, [FFI::Query, :string], CResultVoid
20
+ attach_function :next_event, :polar_next_query_event, [FFI::Query], CResultString
21
+ attach_function :next_message, :polar_next_query_message, [FFI::Query], CResultString
22
+ attach_function :source, :polar_query_source_info, [FFI::Query], CResultString
23
23
  attach_function :free, :query_free, [FFI::Query], :int32
24
- attach_function :bind, :polar_bind, [FFI::Query, :string, :string], :int32
24
+ attach_function :result_free, :result_free, [:pointer], :int32
25
+ attach_function :bind, :polar_bind, [FFI::Query, :string, :string], CResultVoid
25
26
  end
26
27
  private_constant :Rust
27
28
 
@@ -30,15 +31,15 @@ module Oso
30
31
  def debug_command(cmd)
31
32
  res = Rust.debug_command(self, cmd)
32
33
  process_messages
33
- handle_error if res.zero?
34
+ check_result res
34
35
  end
35
36
 
36
- # @param result [String]
37
+ # @param value [Object]
37
38
  # @param call_id [Integer]
38
39
  # @raise [FFI::Error] if the FFI call returns an error.
39
- def call_result(result, call_id:)
40
- res = Rust.call_result(self, call_id, result)
41
- handle_error if res.zero?
40
+ def call_result(value, call_id:)
41
+ res = Rust.call_result(self, call_id, JSON.dump(value))
42
+ check_result res
42
43
  end
43
44
 
44
45
  # @param result [Boolean]
@@ -47,14 +48,14 @@ module Oso
47
48
  def question_result(result, call_id:)
48
49
  result = result ? 1 : 0
49
50
  res = Rust.question_result(self, call_id, result)
50
- handle_error if res.zero?
51
+ check_result res
51
52
  end
52
53
 
53
54
  # @param message [String]
54
55
  # @raise [FFI::Error] if the FFI call returns an error.
55
56
  def application_error(message)
56
57
  res = Rust.application_error(self, message)
57
- handle_error if res.zero?
58
+ check_result res
58
59
  end
59
60
 
60
61
  # @return [::Oso::Polar::QueryEvent]
@@ -62,18 +63,32 @@ module Oso
62
63
  def next_event
63
64
  event = Rust.next_event(self)
64
65
  process_messages
65
- handle_error if event.null?
66
+ event = check_result event
66
67
 
67
68
  ::Oso::Polar::QueryEvent.new(JSON.parse(event.to_s))
68
69
  end
69
70
 
70
71
  def bind(name, value)
71
72
  res = Rust.bind(self, name, JSON.dump(value))
72
- handle_error if res.zero?
73
+ check_result res
73
74
  end
74
75
 
75
76
  def next_message
76
- Rust.next_message(self)
77
+ check_result Rust.next_message(self)
78
+ end
79
+
80
+ def process_message(message, enrich_message)
81
+ message = JSON.parse(message.to_s)
82
+ kind = message['kind']
83
+ msg = message['msg']
84
+ msg = enrich_message.call(msg)
85
+
86
+ case kind
87
+ when 'Print'
88
+ puts(msg)
89
+ when 'Warning'
90
+ warn(format('[warning] %<msg>s', msg: msg))
91
+ end
77
92
  end
78
93
 
79
94
  def process_messages
@@ -81,7 +96,7 @@ module Oso
81
96
  message = next_message
82
97
  break if message.null?
83
98
 
84
- message.process(enrich_message)
99
+ process_message(message, enrich_message)
85
100
  end
86
101
  end
87
102
 
@@ -89,13 +104,24 @@ module Oso
89
104
  # @raise [FFI::Error] if the FFI call returns an error.
90
105
  def source
91
106
  res = Rust.source(self)
92
- handle_error if res.null?
107
+ res = check_result res
93
108
 
94
109
  res.to_s
95
110
  end
96
111
 
97
- def handle_error
98
- raise FFI::Error.get(enrich_message)
112
+ # Unwrap the result by (a) extracting the pointers for
113
+ # result and error, (b) freeing the result pointers, and then
114
+ # (c) either returning the result pointer, or constructing and
115
+ # raising the error.
116
+ def check_result(res)
117
+ result = res[:result]
118
+ error = res[:error]
119
+ Rust.result_free(res)
120
+
121
+ raise 'internal error: both result and error pointers are not null' if !error.null? && !result.zero?
122
+ raise FFI::Error.get(error, enrich_message) unless error.null?
123
+
124
+ result
99
125
  end
100
126
  end
101
127
  end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
6
- # Wrapper class for Source FFI pointer.
7
- class Source < ::FFI::AutoPointer
8
+ # Wrapper class for Rust strings.
9
+ #
10
+ # Since we force all strings to go through this
11
+ # the `AutoPointer` class will handle
12
+ # actually freeing the string when deleting it
13
+ class RustString < ::FFI::AutoPointer
8
14
  # @return [String]
9
15
  def to_s
10
16
  @to_s ||= read_string.force_encoding('UTF-8')
@@ -14,7 +20,7 @@ module Oso
14
20
  extend ::FFI::Library
15
21
  ffi_lib FFI::LIB_PATH
16
22
 
17
- attach_function :free, :string_free, [Message], :int32
23
+ attach_function :free, :string_free, [RustString], :int32
18
24
  end
19
25
 
20
26
  private_constant :Rust
data/lib/oso/polar/ffi.rb CHANGED
@@ -4,6 +4,7 @@ require 'ffi'
4
4
 
5
5
  module Oso
6
6
  module Polar
7
+ # FFI classes shared between all ffi/*.rb modules
7
8
  module FFI
8
9
  LIB = "#{::FFI::Platform::LIBPREFIX}polar.#{::FFI::Platform::LIBSUFFIX}"
9
10
  RELEASE_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
@@ -18,41 +19,50 @@ module Oso
18
19
 
19
20
  # Wrapper class for Polar FFI pointer + operations.
20
21
  class Polar < ::FFI::AutoPointer
22
+ def zero?
23
+ null?
24
+ end
25
+
21
26
  def self.release(ptr)
22
27
  Rust.free(ptr) unless ptr.null?
23
28
  end
24
29
  end
25
30
  # Wrapper class for Query FFI pointer + operations.
26
31
  class Query < ::FFI::AutoPointer
27
- def self.release(ptr)
28
- Rust.free(ptr) unless ptr.null?
32
+ def zero?
33
+ null?
29
34
  end
30
- end
31
- # Wrapper class for QueryEvent FFI pointer + operations.
32
- class QueryEvent < ::FFI::AutoPointer
35
+
33
36
  def self.release(ptr)
34
37
  Rust.free(ptr) unless ptr.null?
35
38
  end
36
39
  end
37
- # Wrapper class for Error FFI pointer + operations.
38
- class Error < ::FFI::AutoPointer
39
- def self.release(ptr)
40
- Rust.free(ptr) unless ptr.null?
40
+
41
+ # Wrapper class for Rust strings FFI pointer + operations.
42
+ class RustString < ::FFI::AutoPointer
43
+ def zero?
44
+ null?
41
45
  end
42
- end
43
- # Wrapper class for Message FFI pointer + operations.
44
- class Message < ::FFI::AutoPointer
46
+
45
47
  def self.release(ptr)
46
48
  Rust.free(ptr) unless ptr.null?
47
49
  end
48
50
  end
49
51
 
50
- # Wrapper class for Source FFI pointer.
51
- class Source < ::FFI::AutoPointer
52
- def self.release(ptr)
53
- Rust.free(ptr) unless ptr.null?
54
- end
52
+ # Helper method to generate a Result type for different
53
+ # inner types
54
+ def self.result(result_klass)
55
+ Class.new(::FFI::Struct) do
56
+ layout :result, result_klass, :error, RustString
57
+ end.by_ref
55
58
  end
59
+
60
+ # Defines the result type version of
61
+ # each of these structs
62
+ # result(T) => { result: T, error: string }
63
+ CResultVoid = result(:int)
64
+ CResultString = result(RustString)
65
+ CResultQuery = result(Query)
56
66
  end
57
67
  private_constant :FFI
58
68
  end
@@ -60,7 +70,5 @@ end
60
70
 
61
71
  require 'oso/polar/ffi/polar'
62
72
  require 'oso/polar/ffi/query'
63
- require 'oso/polar/ffi/query_event'
64
73
  require 'oso/polar/ffi/error'
65
- require 'oso/polar/ffi/message'
66
- require 'oso/polar/ffi/source'
74
+ require 'oso/polar/ffi/rust_string'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'data'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  # Ruby code reloaders (i.e. the one used by rails) swap out the value of
@@ -67,7 +69,7 @@ module Oso
67
69
  public
68
70
 
69
71
  attr_writer :accept_expression
70
- attr_accessor :build_query, :combine_query, :exec_query
72
+ attr_accessor :build_query, :combine_query, :exec_query, :adapter
71
73
 
72
74
  DEFAULT_COMBINE_QUERY = proc { raise 'implement combine_query to use data filtering' }
73
75
  DEFAULT_BUILD_QUERY = proc { raise 'implement build_query to use data filtering' }
@@ -95,9 +97,10 @@ module Oso
95
97
  # @return [Class]
96
98
  # @raise [UnregisteredClassError] if the class has not been registered.
97
99
  def get_class(name)
98
- raise UnregisteredClassError, name unless types.key? name
100
+ typ = types[name]
101
+ raise UnregisteredClassError, name if typ.nil?
99
102
 
100
- types[name].klass.get
103
+ typ.klass.get
101
104
  end
102
105
 
103
106
  # Store a Ruby class in the {#types} cache.
@@ -337,21 +340,25 @@ module Oso
337
340
  value
338
341
  when 'Number'
339
342
  num = value.values.first
340
- if value.key? 'Float'
343
+ case value.keys.first
344
+ when 'Float'
341
345
  case num
342
346
  when 'Infinity'
343
- return Float::INFINITY
347
+ Float::INFINITY
344
348
  when '-Infinity'
345
- return -Float::INFINITY
349
+ -Float::INFINITY
346
350
  when 'NaN'
347
- return Float::NAN
351
+ Float::NAN
348
352
  else
349
353
  unless value['Float'].is_a? Float # rubocop:disable Metrics/BlockNesting
350
354
  raise PolarRuntimeError, "Expected a floating point number, got \"#{value['Float']}\""
351
355
  end
356
+
357
+ num
352
358
  end
359
+ else
360
+ num
353
361
  end
354
- num
355
362
  when 'List'
356
363
  value.map { |el| to_ruby(el) }
357
364
  when 'Dictionary'
@@ -389,6 +396,10 @@ module Oso
389
396
  get_instance(Regexp.last_match[1].to_i).to_s
390
397
  end
391
398
  end
399
+
400
+ def use_new_data_filtering?
401
+ !adapter.nil?
402
+ end
392
403
  end
393
404
  end
394
405
  end
@@ -84,20 +84,8 @@ module Oso
84
84
  @ffi_polar
85
85
  end
86
86
 
87
- # get the (maybe user-supplied) name of a class.
88
- # kind of a hack because of class autoreloading.
89
- def get_class_name(klass) # rubocop:disable Metrics/AbcSize
90
- if host.types.key? klass
91
- host.types[klass].name
92
- elsif host.types.key? klass.name
93
- host.types[klass.name].name
94
- else
95
- rec = host.types.values.find { |v| v.klass.get == klass }
96
- raise "Unknown class `#{klass}`" if rec.nil?
97
-
98
- host.types[klass] = rec
99
- rec.name
100
- end
87
+ def name_to_class(class_name)
88
+ host.types[class_name].klass.get
101
89
  end
102
90
 
103
91
  # Clear all rules and rule sources from the current Polar instance
@@ -259,10 +247,89 @@ module Oso
259
247
 
260
248
  private
261
249
 
250
+ # new/old data filtering core API shared logic
251
+ def partial_query(actor, action, resource_cls) # rubocop:disable Metrics/MethodLength
252
+ var_name = 'resource'
253
+ resource = Variable.new var_name
254
+
255
+ partials = query_rule(
256
+ 'allow',
257
+ actor,
258
+ action,
259
+ resource,
260
+ bindings: { var_name => type_constraint(resource, resource_cls) },
261
+ accept_expression: true
262
+ )
263
+
264
+ partials.each_with_object([]) do |result, out|
265
+ result.each do |key, val|
266
+ out.push prefilter_isas(key, val)
267
+ end
268
+ end
269
+ end
270
+
271
+ def new_authorized_query(actor, action, resource_class)
272
+ partials = partial_query(actor, action, resource_class)
273
+ types = host.serialize_types
274
+ class_name = class_to_name resource_class
275
+ plan = ffi.build_data_filter(types, partials, 'resource', class_name)
276
+ filter = ::Oso::Polar::Data::Filter.parse(self, plan)
277
+ host.adapter.build_query filter
278
+ end
279
+
280
+ def old_authorized_query(actor, action, resource_cls)
281
+ results = partial_query(actor, action, resource_cls)
282
+ ::Oso::Polar::DataFiltering::FilterPlan
283
+ .parse(self, results, class_to_name(resource_cls))
284
+ .build_query
285
+ end
286
+
287
+ # handle Isa constraints in a partial query
288
+ def prefilter_isas(key, val) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
289
+ # this will usually be the case! sometimes not, if it's an instance.
290
+ if val.is_a?(Expression) && val.operator == 'And'
291
+ # get the isas
292
+ isas, othas = val.args.partition do |expr|
293
+ expr.operator == 'Isa' &&
294
+ expr.args[1].is_a?(Pattern) &&
295
+ expr.args[1].fields.empty?
296
+ end
297
+
298
+ # drop all the isas we can verify now, keep everything else
299
+ othas += isas.reject do |isa|
300
+ isa.args[0].is_a? name_to_class isa.args[1].tag
301
+ end
302
+
303
+ # TODO(gw) check the rest of them instead of just adding them?
304
+ val.args = othas
305
+ end
306
+ val = host.to_polar val
307
+ { 'bindings' => { key => val } }
308
+ end
309
+
310
+ # get the (maybe user-supplied) name of a class.
311
+ # kind of a hack because of class autoreloading.
312
+ def class_to_name(klass) # rubocop:disable Metrics/AbcSize
313
+ if (rec = host.types[klass]) || (rec = host.types[klass.name])
314
+ rec.name
315
+ elsif (rec = host.types.values.find { |v| v.klass.get == klass })
316
+ host.types[klass] = rec
317
+ rec.name
318
+ else
319
+ raise NameError, "Unknown class `#{klass}`"
320
+ end
321
+ end
322
+
323
+ def try_class_to_name(klass)
324
+ class_to_name klass
325
+ rescue NameError
326
+ nil
327
+ end
328
+
262
329
  def type_constraint(var, cls)
263
330
  Expression.new(
264
331
  'And',
265
- [Expression.new('Isa', [var, Pattern.new(get_class_name(cls), {})])]
332
+ [Expression.new('Isa', [var, Pattern.new(class_to_name(cls), {})])]
266
333
  )
267
334
  end
268
335
 
@@ -94,8 +94,7 @@ module Oso
94
94
  else
95
95
  instance.__send__(attribute, *args, **kwargs)
96
96
  end
97
- result = JSON.dump(host.to_polar(result))
98
- call_result(result, call_id: call_id)
97
+ call_result(host.to_polar(result), call_id: call_id)
99
98
  rescue ArgumentError, NoMethodError => e
100
99
  application_error(e.message)
101
100
  call_result(nil, call_id: call_id)
@@ -143,7 +142,7 @@ module Oso
143
142
  calls[call_id] = value.lazy
144
143
  end
145
144
 
146
- result = JSON.dump(next_call_result(call_id))
145
+ result = next_call_result(call_id)
147
146
  call_result(result, call_id: call_id)
148
147
  rescue StopIteration
149
148
  call_result(nil, call_id: call_id)
@@ -245,12 +244,32 @@ module Oso
245
244
 
246
245
  def handle_relationship(call_id, instance, rel) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
247
246
  typ = host.types[rel.other_type]
248
- constraint = ::Oso::Polar::DataFiltering::Filter.new(
249
- kind: 'Eq',
250
- field: rel.other_field,
251
- value: instance.send(rel.my_field)
252
- )
253
- res = typ.exec_query[typ.build_query[[constraint]]]
247
+
248
+ if host.use_new_data_filtering?
249
+ cls = typ.klass.get
250
+
251
+ condition = ::Oso::Polar::Data::Filter::Condition.new(
252
+ ::Oso::Polar::Data::Filter::Projection.new(cls, rel.other_field),
253
+ 'Eq',
254
+ instance.send(rel.my_field)
255
+ )
256
+
257
+ filter = ::Oso::Polar::Data::Filter.new(
258
+ model: cls,
259
+ relations: [],
260
+ conditions: [[condition]],
261
+ types: host.types
262
+ )
263
+
264
+ res = host.adapter.execute_query host.adapter.build_query(filter)
265
+ else
266
+ constraint = ::Oso::Polar::DataFiltering::Filter.new(
267
+ kind: 'Eq',
268
+ field: rel.other_field,
269
+ value: instance.send(rel.my_field)
270
+ )
271
+ res = typ.exec_query[typ.build_query[[constraint]]]
272
+ end
254
273
 
255
274
  if rel.kind == 'one'
256
275
  raise "multiple parents: #{res}" unless res.length == 1
@@ -258,8 +277,7 @@ module Oso
258
277
  res = res[0]
259
278
  end
260
279
 
261
- res = JSON.dump host.to_polar res
262
- call_result(res, call_id: call_id)
280
+ call_result(host.to_polar(res), call_id: call_id)
263
281
  end
264
282
  end
265
283
  end
data/lib/oso/polar.rb CHANGED
@@ -11,6 +11,7 @@ require 'oso/polar/query'
11
11
  require 'oso/polar/query_event'
12
12
  require 'oso/polar/variable'
13
13
  require 'oso/polar/data_filtering'
14
+ require 'oso/polar/data'
14
15
 
15
16
  module Oso
16
17
  # Top-level namespace for Polar language library.
data/lib/oso/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oso
4
- VERSION = '0.23.0'
4
+ VERSION = '0.24.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oso-oso
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oso Security, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-10 00:00:00.000000000 Z
11
+ date: 2021-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -162,16 +162,18 @@ files:
162
162
  - lib/oso/errors.rb
163
163
  - lib/oso/oso.rb
164
164
  - lib/oso/polar.rb
165
+ - lib/oso/polar/data.rb
166
+ - lib/oso/polar/data/adapter.rb
167
+ - lib/oso/polar/data/adapter/active_record_adapter.rb
168
+ - lib/oso/polar/data/filter.rb
165
169
  - lib/oso/polar/data_filtering.rb
166
170
  - lib/oso/polar/errors.rb
167
171
  - lib/oso/polar/expression.rb
168
172
  - lib/oso/polar/ffi.rb
169
173
  - lib/oso/polar/ffi/error.rb
170
- - lib/oso/polar/ffi/message.rb
171
174
  - lib/oso/polar/ffi/polar.rb
172
175
  - lib/oso/polar/ffi/query.rb
173
- - lib/oso/polar/ffi/query_event.rb
174
- - lib/oso/polar/ffi/source.rb
176
+ - lib/oso/polar/ffi/rust_string.rb
175
177
  - lib/oso/polar/host.rb
176
178
  - lib/oso/polar/pattern.rb
177
179
  - lib/oso/polar/polar.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Oso
6
- module Polar
7
- module FFI
8
- # Wrapper class for Message FFI pointer + operations.
9
- class Message < ::FFI::AutoPointer
10
- # @return [String]
11
- def to_s
12
- @to_s ||= read_string.force_encoding('UTF-8')
13
- end
14
-
15
- Rust = Module.new do
16
- extend ::FFI::Library
17
- ffi_lib FFI::LIB_PATH
18
-
19
- attach_function :free, :string_free, [Message], :int32
20
- end
21
-
22
- def process(enrich_message)
23
- message = JSON.parse(to_s)
24
- kind = message['kind']
25
- msg = message['msg']
26
- msg = enrich_message.call(msg)
27
-
28
- case kind
29
- when 'Print'
30
- puts(msg)
31
- when 'Warning'
32
- warn(format('[warning] %<msg>s', msg: msg))
33
- end
34
- end
35
-
36
- private_constant :Rust
37
- end
38
- end
39
- end
40
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Oso
4
- module Polar
5
- module FFI
6
- # Wrapper class for QueryEvent FFI pointer + operations.
7
- class QueryEvent < ::FFI::AutoPointer
8
- # @return [String]
9
- def to_s
10
- @to_s ||= read_string.force_encoding('UTF-8')
11
- end
12
-
13
- Rust = Module.new do
14
- extend ::FFI::Library
15
- ffi_lib FFI::LIB_PATH
16
-
17
- attach_function :free, :string_free, [FFI::QueryEvent], :int32
18
- end
19
- private_constant :Rust
20
- end
21
- end
22
- end
23
- end