oso-oso 0.2.5 → 0.2.6

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: 9e511f86c5a330c1bb071824398085362def8ba7
4
- data.tar.gz: da3baaad23fb6e28c8ef0558bbf1deb659c03700
3
+ metadata.gz: 8c00a51f40789d269beb2d63e29ad12b76565d94
4
+ data.tar.gz: 8f66dfdb31532125fd3593ddcfb7c3e76fd81df5
5
5
  SHA512:
6
- metadata.gz: 9d55c689e8ffc0e3f7b533f54d3e622f77bd7f563182fc7b9dfc047a7a9b1554ed02711fbb8326f55fed76529dde2040d6f829bd4439a63773c9bb58ffb8c8a3
7
- data.tar.gz: 60b1c8f83803656373a108a64159636292bc91c1840292e7c01ea0c74961533d076ceaa6576010ecdf9dc062bc60c6b112e9900b726db227a322aa52c41f40e0
6
+ metadata.gz: 6ac0a230d40ecab86a7896249eb875fa43a16705a0594c9b55acd9d21215bff572ce1abb18da0593455f1a3c749354453808b779bed3dbf14e171f8b26b7511c
7
+ data.tar.gz: 910cccfb441f515b20bcd1c21fc02a3f293b4445ed4526e2b8a2a921d97062df23156bc2726587269139a79edd687bd8d5b9fa79776bbc5910045f2205e58923
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.2.5)
4
+ oso-oso (0.2.6)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
data/Makefile CHANGED
@@ -8,3 +8,6 @@ install: rust
8
8
 
9
9
  test: install
10
10
  bundle exec rake spec
11
+
12
+ repl:
13
+ bundle exec oso
data/bin/oso ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env -S bundle exec ruby
2
+ #-*-ruby-*-
3
+
4
+ require 'bundler/setup'
5
+ require 'oso'
6
+
7
+ Oso.new.repl(load: true)
Binary file
Binary file
@@ -1,47 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'polar/polar'
4
+
3
5
  module Oso
4
6
  # Oso authorization API.
5
- class Oso
7
+ class Oso < Polar::Polar
6
8
  def initialize
7
- @polar = ::Oso::Polar.new
9
+ super
8
10
  register_class(Http, name: 'Http')
9
11
  register_class(PathMapper, name: 'PathMapper')
10
12
  end
11
13
 
12
- def load_file(file)
13
- polar.load_file(file)
14
- end
15
-
16
- def load_str(str)
17
- polar.load_str(str)
18
- end
19
-
20
- def register_class(cls, name: nil) # rubocop:disable Naming/MethodParameterName
21
- if block_given?
22
- polar.register_class(cls, name: name, from_polar: Proc.new)
23
- else
24
- polar.register_class(cls, name: name)
25
- end
26
- end
27
-
28
14
  def allow(actor:, action:, resource:)
29
- polar.query_pred('allow', args: [actor, action, resource]).next
15
+ query_predicate('allow', actor, action, resource).next
30
16
  true
31
17
  rescue StopIteration
32
18
  false
33
19
  end
34
-
35
- def query_predicate(name, *args)
36
- polar.query_pred(name, args: args)
37
- end
38
-
39
- def load_queued_files
40
- polar.load_queued_files
41
- end
42
-
43
- private
44
-
45
- attr_reader :polar
46
20
  end
47
21
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'oso/polar/errors'
4
4
  require 'oso/polar/ffi'
5
+ require 'oso/polar/host'
5
6
  require 'oso/polar/polar'
6
7
  require 'oso/polar/predicate'
7
8
  require 'oso/polar/query'
@@ -40,6 +40,7 @@ module Oso
40
40
  class DuplicateInstanceRegistrationError < PolarRuntimeError; end
41
41
  class InvalidCallError < PolarRuntimeError; end
42
42
  class InvalidConstructorError < PolarRuntimeError; end
43
+ class InvalidQueryTypeError < PolarRuntimeError; end
43
44
  class InlineQueryFailedError < PolarRuntimeError; end
44
45
  class NullByteInPolarFileError < PolarRuntimeError; end
45
46
  class UnexpectedPolarTypeError < PolarRuntimeError; end
@@ -6,9 +6,11 @@ module Oso
6
6
  module Polar
7
7
  module FFI
8
8
  LIB = ::FFI::Platform::LIBPREFIX + 'polar.' + ::FFI::Platform::LIBSUFFIX
9
- LIB_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
10
- # @TODO: Fall back to this if there's no release build libs. Easier for dev.
11
- # LIB_PATH = File.expand_path(File.join(__dir__, "../../../../../target/debug/#{LIB}"))
9
+ RELEASE_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
10
+ DEV_PATH = File.expand_path(File.join(__dir__, "../../../../../target/debug/#{LIB}"))
11
+ # If the lib exists in the ext/ dir, use it. Otherwise, fall back to
12
+ # checking the local Rust target dir.
13
+ LIB_PATH = File.file?(RELEASE_PATH) ? RELEASE_PATH : DEV_PATH
12
14
 
13
15
  # Wrapper classes defined upfront to fix Ruby loading issues. Actual
14
16
  # implementations live in the sibling `ffi/` directory and are `require`d
@@ -15,7 +15,6 @@ module Oso
15
15
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
16
16
  attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string], FFI::Query
17
17
  attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string], FFI::Query
18
- attach_function :new_query_from_repl, :polar_query_from_repl, [FFI::Polar], FFI::Query
19
18
  attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
20
19
  attach_function :free, :polar_free, [FFI::Polar], :int32
21
20
  end
@@ -76,15 +75,6 @@ module Oso
76
75
  query
77
76
  end
78
77
 
79
- # @return [FFI::Query]
80
- # @raise [FFI::Error] if the FFI call returns an error.
81
- def new_query_from_repl
82
- query = Rust.new_query_from_repl(self)
83
- raise FFI::Error.get if query.null?
84
-
85
- query
86
- end
87
-
88
78
  # @param name [String]
89
79
  # @param value [Hash<String, Object>]
90
80
  # @raise [FFI::Error] if the FFI call returns an error.
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Translate between Polar and the host language (Ruby).
6
+ class Host
7
+ protected
8
+ # @return [FFI::Polar]
9
+ attr_reader :ffi_polar
10
+ # @return [Hash<String, Class>]
11
+ attr_reader :classes
12
+ # @return [Hash<String, Object>]
13
+ attr_reader :constructors
14
+ # @return [Hash<Integer, Object>]
15
+ attr_reader :instances
16
+
17
+ public
18
+ def initialize(ffi_polar)
19
+ @ffi_polar = ffi_polar
20
+ @classes = {}
21
+ @constructors = {}
22
+ @instances = {}
23
+ end
24
+
25
+ def initialize_copy(other)
26
+ @ffi_polar = other.ffi_polar
27
+ @classes = other.classes.dup
28
+ @constructors = other.constructors.dup
29
+ @instances = other.instances.dup
30
+ end
31
+
32
+ # Fetch a Ruby class from the {#classes} cache.
33
+ #
34
+ # @param name [String]
35
+ # @return [Class]
36
+ # @raise [UnregisteredClassError] if the class has not been registered.
37
+ def get_class(name)
38
+ raise UnregisteredClassError, name unless classes.key? name
39
+
40
+ classes[name]
41
+ end
42
+
43
+ # Store a Ruby class in the {#classes} cache.
44
+ #
45
+ # @param cls [Class] the class to cache
46
+ # @param name [String] the name to cache the class as. Defaults to the name of the class.
47
+ # @return [String] the name the class is cached as.
48
+ # @raise [UnregisteredClassError] if the class has not been registered.
49
+ def cache_class(cls, name:, constructor:)
50
+ name = cls.name if name.nil?
51
+ raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
52
+
53
+ classes[name] = cls
54
+ if constructor.nil?
55
+ constructors[name] = :new
56
+ elsif constructor.respond_to? :call
57
+ constructors[name] = constructor
58
+ else
59
+ raise InvalidConstructorError
60
+ end
61
+ name
62
+ end
63
+
64
+ # Fetch a constructor from the {#constructors} cache.
65
+ #
66
+ # @param name [String]
67
+ # @return [Symbol] if constructor is the default of `:new`.
68
+ # @return [Proc] if a custom constructor was registered.
69
+ # @raise [UnregisteredConstructorError] if the constructor has not been registered.
70
+ def get_constructor(name)
71
+ raise MissingConstructorError, name unless constructors.key? name
72
+
73
+ constructors[name]
74
+ end
75
+
76
+ # Check if an instance has been cached.
77
+ #
78
+ # @param id [Integer]
79
+ # @return [Boolean]
80
+ def instance?(id)
81
+ case id
82
+ when Integer
83
+ instances.key? id
84
+ else
85
+ instances.value? id
86
+ end
87
+ end
88
+
89
+ # Fetch a Ruby instance from the {#instances} cache.
90
+ #
91
+ # @param id [Integer]
92
+ # @return [Object]
93
+ # @raise [UnregisteredInstanceError] if the ID has not been registered.
94
+ def get_instance(id)
95
+ raise UnregisteredInstanceError, id unless instance? id
96
+
97
+ instances[id]
98
+ end
99
+
100
+ # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
101
+ #
102
+ # @param instance [Object]
103
+ # @param id [Integer]
104
+ # @return [Integer]
105
+ def cache_instance(instance, id: nil)
106
+ id = ffi_polar.new_id if id.nil?
107
+ instances[id] = instance
108
+ id
109
+ end
110
+
111
+ # Construct and cache a Ruby instance.
112
+ #
113
+ # @param cls_name [String]
114
+ # @param fields [Hash<String, Hash>]
115
+ # @param id [Integer]
116
+ # @raise [PolarRuntimeError] if instance construction fails.
117
+ def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
118
+ constructor = get_constructor(cls_name)
119
+ fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
120
+ instance = if constructor == :new
121
+ if fields.empty?
122
+ get_class(cls_name).__send__(:new)
123
+ else
124
+ get_class(cls_name).__send__(:new, **fields)
125
+ end
126
+ elsif fields.empty?
127
+ constructor.call
128
+ else
129
+ constructor.call(**fields)
130
+ end
131
+ cache_instance(instance, id: id)
132
+ rescue StandardError => e
133
+ raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
134
+ end
135
+
136
+ # Check if the left class is more specific than the right class
137
+ # with respect to the given instance.
138
+ #
139
+ # @param instance_id [Integer]
140
+ # @param left_tag [String]
141
+ # @param right_tag [String]
142
+ # @return [Boolean]
143
+ def subspecializer?(instance_id, left_tag:, right_tag:)
144
+ mro = get_instance(instance_id).class.ancestors
145
+ mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
146
+ rescue StandardError
147
+ false
148
+ end
149
+
150
+ # Check if instance is an instance of class.
151
+ #
152
+ # @param instance_id [Integer]
153
+ # @param class_tag [String]
154
+ # @return [Boolean]
155
+ def isa?(instance_id, class_tag:)
156
+ instance = get_instance(instance_id)
157
+ cls = get_class(class_tag)
158
+ instance.is_a? cls
159
+ rescue PolarRuntimeError
160
+ false
161
+ end
162
+
163
+ # Check if two instances unify
164
+ #
165
+ # @param left_instance_id [Integer]
166
+ # @param right_instance_id [Integer]
167
+ # @return [Boolean]
168
+ def unify?(left_instance_id, right_instance_id)
169
+ left_instance = get_instance(left_instance_id)
170
+ right_instance = get_instance(right_instance_id)
171
+ left_instance == right_instance
172
+ rescue PolarRuntimeError
173
+ false
174
+ end
175
+
176
+ # Turn a Ruby value into a Polar term that's ready to be sent across the
177
+ # FFI boundary.
178
+ #
179
+ # @param value [Object]
180
+ # @return [Hash<String, Object>]
181
+ def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
182
+ value = case true # rubocop:disable Lint/LiteralAsCondition
183
+ when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
184
+ { 'Boolean' => value }
185
+ when value.instance_of?(Integer)
186
+ { 'Number' => { 'Integer' => value } }
187
+ when value.instance_of?(Float)
188
+ { 'Number' => { 'Float' => value } }
189
+ when value.instance_of?(String)
190
+ { 'String' => value }
191
+ when value.instance_of?(Array)
192
+ { 'List' => value.map { |el| to_polar_term(el) } }
193
+ when value.instance_of?(Hash)
194
+ { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
195
+ when value.instance_of?(Predicate)
196
+ { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
197
+ when value.instance_of?(Variable)
198
+ # This is supported so that we can query for unbound variables
199
+ { 'Variable' => value }
200
+ else
201
+ { 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
202
+ end
203
+ { 'value' => value }
204
+ end
205
+
206
+ # Turn a Polar term passed across the FFI boundary into a Ruby value.
207
+ #
208
+ # @param data [Hash<String, Object>]
209
+ # @option data [Integer] :id
210
+ # @option data [Integer] :offset Character offset of the term in its source string.
211
+ # @option data [Hash<String, Object>] :value
212
+ # @return [Object]
213
+ # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
214
+ def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
215
+ tag, value = data['value'].first
216
+ case tag
217
+ when 'String', 'Boolean'
218
+ value
219
+ when 'Number'
220
+ value.values.first
221
+ when 'List'
222
+ value.map { |el| to_ruby(el) }
223
+ when 'Dictionary'
224
+ value['fields'].transform_values { |v| to_ruby(v) }
225
+ when 'ExternalInstance'
226
+ get_instance(value['instance_id'])
227
+ when 'Call'
228
+ Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
229
+ else
230
+ raise UnexpectedPolarTypeError, tag
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -1,21 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'pp'
4
5
  require 'set'
5
6
 
6
7
  module Oso
7
8
  module Polar
8
9
  # Create and manage an instance of the Polar runtime.
9
- class Polar # rubocop:disable Metrics/ClassLength
10
+ class Polar
11
+ # @return [Host]
12
+ attr_reader :host
13
+
10
14
  def initialize
11
- @ffi_instance = FFI::Polar.create
12
- @calls = {}
13
- @classes = {}
14
- @constructors = {}
15
- @instances = {}
15
+ @ffi_polar = FFI::Polar.create
16
+ @host = Host.new(ffi_polar)
16
17
  @load_queue = Set.new
17
18
  end
18
19
 
20
+ # Replace the current Polar instance but retain all registered classes and constructors.
21
+ def clear
22
+ load_queue.clear
23
+ @ffi_polar = FFI::Polar.create
24
+ end
25
+
19
26
  # Enqueue a Polar policy file for loading into the KB.
20
27
  #
21
28
  # @param name [String]
@@ -38,23 +45,32 @@ module Oso
38
45
  def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
39
46
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
40
47
 
41
- ffi_instance.load_str(str, filename: filename)
48
+ ffi_polar.load_str(str, filename: filename)
42
49
  loop do
43
- next_query = ffi_instance.next_inline_query
50
+ next_query = ffi_polar.next_inline_query
44
51
  break if next_query.nil?
45
52
 
46
53
  begin
47
- Query.new(next_query, polar: self).results.next
54
+ Query.new(next_query, host: host).results.next
48
55
  rescue StopIteration
49
56
  raise InlineQueryFailedError
50
57
  end
51
58
  end
52
59
  end
53
60
 
54
- # Replace the current Polar instance but retain all registered classes and
55
- # constructors.
56
- def clear
57
- @ffi_instance = FFI::Polar.create
61
+ # Query for a predicate, parsing it if necessary.
62
+ def query(query)
63
+ load_queued_files
64
+ new_host = host.dup
65
+ case query
66
+ when String
67
+ ffi_query = ffi_polar.new_query_from_str(query)
68
+ when Predicate
69
+ ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
70
+ else
71
+ raise InvalidQueryTypeError
72
+ end
73
+ Query.new(ffi_query, host: new_host).results
58
74
  end
59
75
 
60
76
  # Query for a predicate.
@@ -62,245 +78,69 @@ module Oso
62
78
  # @param name [String]
63
79
  # @param args [Array<Object>]
64
80
  # @raise [Error] if the FFI call raises one.
65
- def query_pred(name, args:)
66
- clear_query_state
67
- load_queued_files
68
- pred = Predicate.new(name, args: args)
69
- query_ffi_instance = ffi_instance.new_query_from_term(to_polar_term(pred))
70
- Query.new(query_ffi_instance, polar: self).results
81
+ def query_predicate(name, *args)
82
+ query(Predicate.new(name, args: args))
71
83
  end
72
84
 
73
85
  # Start a REPL session.
74
86
  #
75
87
  # @raise [Error] if the FFI call raises one.
76
- def repl # rubocop:disable Metrics/MethodLength
77
- clear_query_state
88
+ def repl(load: false) # rubocop:disable Metrics/MethodLength
89
+ ARGV.map { |f| load_file(f) } if load
78
90
  load_queued_files
91
+
79
92
  loop do
80
- query = Query.new(ffi_instance.new_query_from_repl, polar: self)
81
- results = query.results.to_a
93
+ print('> ')
94
+ begin
95
+ query = STDIN.readline.chomp.chomp(';')
96
+ rescue EOFError
97
+ return
98
+ end
99
+
100
+ begin
101
+ ffi_query = ffi_polar.new_query_from_str(query)
102
+ rescue ParseError => e
103
+ puts("Parse error: " + e.to_s)
104
+ next
105
+ end
106
+
107
+ begin
108
+ results = Query.new(ffi_query, host: host).results.to_a
109
+ rescue PolarRuntimeError => e
110
+ puts(e.to_s)
111
+ next
112
+ end
113
+
82
114
  if results.empty?
83
- puts 'False'
115
+ pp false
84
116
  else
85
117
  results.each do |result|
86
- puts result
118
+ if result.empty?
119
+ pp true
120
+ else
121
+ pp result
122
+ end
87
123
  end
88
124
  end
89
125
  end
90
126
  end
91
127
 
92
- # Get a unique ID from Polar.
93
- #
94
- # @return [Integer]
95
- # @raise [Error] if the FFI call raises one.
96
- def new_id
97
- ffi_instance.new_id
98
- end
99
-
100
128
  # Register a Ruby class with Polar.
101
129
  #
102
130
  # @param cls [Class]
103
131
  # @param name [String]
104
132
  # @param from_polar [Proc]
105
133
  # @raise [InvalidConstructorError] if provided an invalid 'from_polar' constructor.
106
- def register_class(cls, name: nil, from_polar: nil) # rubocop:disable Naming/MethodParameterName
107
- # TODO(gj): should this take 3 args: cls (Class), constructor_cls
108
- # (Option<Class>) that defaults to cls, and constructor_method
109
- # (Option<Symbol>) that defaults to :new?
110
- name = cls.name if name.nil?
111
- raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
112
-
113
- classes[name] = cls
114
- if from_polar.nil?
115
- constructors[name] = :new
116
- elsif from_polar.respond_to? :call
117
- constructors[name] = from_polar
118
- else
119
- raise InvalidConstructorError
134
+ def register_class(cls, name: nil, from_polar: nil)
135
+ if block_given?
136
+ from_polar = Proc.new
120
137
  end
121
-
138
+ name = host.cache_class(cls, name: name, constructor: from_polar)
122
139
  register_constant(name, value: cls)
123
140
  end
124
141
 
125
142
  def register_constant(name, value:)
126
- ffi_instance.register_constant(name, value: to_polar_term(value))
127
- end
128
-
129
- # Register a Ruby method call, wrapping the call result in a generator if
130
- # it isn't already one.
131
- #
132
- # @param method [#to_sym]
133
- # @param call_id [Integer]
134
- # @param instance [Hash]
135
- # @param args [Array<Hash>]
136
- # @raise [InvalidCallError] if the method doesn't exist on the instance or
137
- # the args passed to the method are invalid.
138
- def register_call(method, call_id:, instance:, args:)
139
- return if calls.key?(call_id)
140
-
141
- args = args.map { |a| to_ruby(a) }
142
- if instance["value"].has_key? "ExternalInstance"
143
- instance_id = instance["value"]["ExternalInstance"]["instance_id"]
144
- instance = get_instance(instance_id)
145
- else
146
- instance = to_ruby(instance)
147
- end
148
- result = instance.__send__(method, *args)
149
- result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
150
- calls[call_id] = result.lazy
151
- rescue ArgumentError, NoMethodError
152
- raise InvalidCallError
153
- end
154
-
155
- # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
156
- #
157
- # @param id [Integer]
158
- # @return [Hash]
159
- # @raise [StopIteration] if the call has been exhausted.
160
- def next_call_result(id)
161
- to_polar_term(calls[id].next)
162
- end
163
-
164
- # Construct and cache a Ruby instance.
165
- #
166
- # @param cls_name [String]
167
- # @param fields [Hash<String, Hash>]
168
- # @param id [Integer]
169
- # @raise [PolarRuntimeError] if instance construction fails.
170
- def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
171
- constructor = get_constructor(cls_name)
172
- fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
173
- instance = if constructor == :new
174
- if fields.empty?
175
- get_class(cls_name).__send__(:new)
176
- else
177
- get_class(cls_name).__send__(:new, **fields)
178
- end
179
- elsif fields.empty?
180
- constructor.call
181
- else
182
- constructor.call(**fields)
183
- end
184
- cache_instance(instance, id: id)
185
- rescue StandardError => e
186
- raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
187
- end
188
-
189
- # Check if an instance has been cached.
190
- #
191
- # @param id [Integer]
192
- # @return [Boolean]
193
- def instance?(id)
194
- instances.key? id
195
- end
196
-
197
- # Fetch a Ruby instance from the {#instances} cache.
198
- #
199
- # @param id [Integer]
200
- # @return [Object]
201
- # @raise [UnregisteredInstanceError] if the ID has not been registered.
202
- def get_instance(id)
203
- raise UnregisteredInstanceError, id unless instance? id
204
-
205
- instances[id]
206
- end
207
-
208
- # Check if the left class is more specific than the right class for the
209
- # given instance.
210
- #
211
- # @param instance_id [Integer]
212
- # @param left_tag [String]
213
- # @param right_tag [String]
214
- # @return [Boolean]
215
- def subspecializer?(instance_id, left_tag:, right_tag:)
216
- mro = get_instance(instance_id).class.ancestors
217
- mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
218
- rescue StandardError
219
- false
220
- end
221
-
222
- # Check if instance is an instance of class.
223
- #
224
- # @param instance_id [Integer]
225
- # @param class_tag [String]
226
- # @return [Boolean]
227
- def isa?(instance_id, class_tag:)
228
- instance = get_instance(instance_id)
229
- cls = get_class(class_tag)
230
- instance.is_a? cls
231
- rescue PolarRuntimeError
232
- false
233
- end
234
-
235
- # Check if two instances unify
236
- #
237
- # @param left_instance_id [Integer]
238
- # @param right_instance_id [Integer]
239
- # @return [Boolean]
240
- def unify?(left_instance_id, right_instance_id)
241
- left_instance = get_instance(left_instance_id)
242
- right_instance = get_instance(right_instance_id)
243
- left_instance == right_instance
244
- rescue PolarRuntimeError
245
- false
246
- end
247
-
248
- # Turn a Ruby value into a Polar term that's ready to be sent across the
249
- # FFI boundary.
250
- #
251
- # @param value [Object]
252
- # @return [Hash<String, Object>]
253
- def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
254
- value = case true # rubocop:disable Lint/LiteralAsCondition
255
- when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
256
- { 'Boolean' => value }
257
- when value.instance_of?(Integer)
258
- { 'Number' => { 'Integer' => value } }
259
- when value.instance_of?(Float)
260
- { 'Number' => { 'Float' => value } }
261
- when value.instance_of?(String)
262
- { 'String' => value }
263
- when value.instance_of?(Array)
264
- { 'List' => value.map { |el| to_polar_term(el) } }
265
- when value.instance_of?(Hash)
266
- { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
267
- when value.instance_of?(Predicate)
268
- { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
269
- when value.instance_of?(Variable)
270
- # This is supported so that we can query for unbound variables
271
- { 'Variable' => value }
272
- else
273
- { 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
274
- end
275
- { 'value' => value }
276
- end
277
-
278
- # Turn a Polar term passed across the FFI boundary into a Ruby value.
279
- #
280
- # @param data [Hash<String, Object>]
281
- # @option data [Integer] :id
282
- # @option data [Integer] :offset Character offset of the term in its source string.
283
- # @option data [Hash<String, Object>] :value
284
- # @return [Object]
285
- # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
286
- def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
287
- tag, value = data['value'].first
288
- case tag
289
- when 'String', 'Boolean'
290
- value
291
- when 'Number'
292
- value.values.first
293
- when 'List'
294
- value.map { |el| to_ruby(el) }
295
- when 'Dictionary'
296
- value['fields'].transform_values { |v| to_ruby(v) }
297
- when 'ExternalInstance'
298
- get_instance(value['instance_id'])
299
- when 'Call'
300
- Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
301
- else
302
- raise UnexpectedPolarTypeError, tag
303
- end
143
+ ffi_polar.register_constant(name, value: host.to_polar_term(value))
304
144
  end
305
145
 
306
146
  # Load all queued files, flushing the {#load_queue}.
@@ -313,69 +153,10 @@ module Oso
313
153
 
314
154
  private
315
155
 
316
- # @return [Hash<Integer, Enumerator>]
317
- attr_reader :calls
318
- # @return [Hash<String, Class>]
319
- attr_reader :classes
320
- # @return [Hash<String, Object>]
321
- attr_reader :constructors
322
156
  # @return [FFI::Polar]
323
- attr_reader :ffi_instance
324
- # @return [Hash<Integer, Object>]
325
- attr_reader :instances
157
+ attr_reader :ffi_polar
326
158
  # @return [Array<String>]
327
159
  attr_reader :load_queue
328
-
329
- # Clear the instance and call caches.
330
- def clear_query_state
331
- calls.clear
332
- instances.clear
333
- end
334
-
335
- # Query for a Polar string.
336
- #
337
- # @param str [String]
338
- # @return [Enumerator]
339
- def query_str(str)
340
- clear_query_state
341
- load_queued_files
342
- query_ffi_instance = ffi_instance.new_query_from_str(str)
343
- Query.new(query_ffi_instance, polar: self).results
344
- end
345
-
346
- # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
347
- #
348
- # @param instance [Object]
349
- # @param id [Integer]
350
- # @return [Integer]
351
- def cache_instance(instance, id: nil)
352
- id = new_id if id.nil?
353
- instances[id] = instance
354
- id
355
- end
356
-
357
- # Fetch a Ruby class from the {#classes} cache.
358
- #
359
- # @param name [String]
360
- # @return [Class]
361
- # @raise [UnregisteredClassError] if the class has not been registered.
362
- def get_class(name)
363
- raise UnregisteredClassError, name unless classes.key? name
364
-
365
- classes[name]
366
- end
367
-
368
- # Fetch a constructor from the {#constructors} cache.
369
- #
370
- # @param name [String]
371
- # @return [Symbol] if constructor is the default of `:new`.
372
- # @return [Proc] if a custom constructor was registered.
373
- # @raise [UnregisteredConstructorError] if the constructor has not been registered.
374
- def get_constructor(name)
375
- raise MissingConstructorError, name unless constructors.key? name
376
-
377
- constructors[name]
378
- end
379
160
  end
380
161
  end
381
162
  end
@@ -6,20 +6,58 @@ module Oso
6
6
  class Query
7
7
  attr_reader :results
8
8
 
9
- # @param ffi_instance [FFI::Query]
10
- # @param polar [Polar]
11
- def initialize(ffi_instance, polar:)
12
- @ffi_instance = ffi_instance
13
- @polar = polar
9
+ # @param ffi_query [FFI::Query]
10
+ # @param ffi_polar [FFI::Polar]
11
+ def initialize(ffi_query, host:)
12
+ @calls = {}
13
+ @ffi_query = ffi_query
14
+ @host = host
14
15
  @results = start
15
16
  end
16
17
 
17
18
  private
18
19
 
20
+ # @return [Hash<Integer, Enumerator>]
21
+ attr_reader :calls
19
22
  # @return [FFI::Query]
20
- attr_reader :ffi_instance
21
- # @return [Polar]
22
- attr_reader :polar
23
+ attr_reader :ffi_query
24
+ # @return [Host]
25
+ attr_reader :host
26
+
27
+ # Send result of predicate check across FFI boundary.
28
+ #
29
+ # @param result [Boolean]
30
+ # @param call_id [Integer]
31
+ # @raise [Error] if the FFI call raises one.
32
+ def question_result(result, call_id:)
33
+ ffi_query.question_result(result, call_id: call_id)
34
+ end
35
+
36
+ # Register a Ruby method call, wrapping the call result in a generator if
37
+ # it isn't already one.
38
+ #
39
+ # @param method [#to_sym]
40
+ # @param call_id [Integer]
41
+ # @param instance [Hash]
42
+ # @param args [Array<Hash>]
43
+ # @raise [InvalidCallError] if the method doesn't exist on the instance or
44
+ # the args passed to the method are invalid.
45
+ def register_call(method, call_id:, instance:, args:)
46
+ return if calls.key?(call_id)
47
+
48
+ args = args.map { |a| host.to_ruby(a) }
49
+ if instance['value'].key? 'ExternalInstance'
50
+ instance_id = instance['value']['ExternalInstance']['instance_id']
51
+ instance = host.get_instance(instance_id)
52
+ else
53
+ instance = host.to_ruby(instance)
54
+ end
55
+ result = instance.__send__(method, *args)
56
+ result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
57
+ calls[call_id] = result.lazy
58
+ rescue ArgumentError, NoMethodError
59
+ raise InvalidCallError
60
+ end
23
61
 
24
62
  # Send next result of Ruby method call across FFI boundary.
25
63
  #
@@ -27,16 +65,16 @@ module Oso
27
65
  # @param call_id [Integer]
28
66
  # @raise [Error] if the FFI call raises one.
29
67
  def call_result(result, call_id:)
30
- ffi_instance.call_result(result, call_id: call_id)
68
+ ffi_query.call_result(result, call_id: call_id)
31
69
  end
32
70
 
33
- # Send result of predicate check across FFI boundary.
71
+ # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
34
72
  #
35
- # @param result [Boolean]
36
- # @param call_id [Integer]
37
- # @raise [Error] if the FFI call raises one.
38
- def question_result(result, call_id:)
39
- ffi_instance.question_result(result, call_id: call_id)
73
+ # @param id [Integer]
74
+ # @return [Hash]
75
+ # @raise [StopIteration] if the call has been exhausted.
76
+ def next_call_result(id)
77
+ host.to_polar_term(calls[id].next)
40
78
  end
41
79
 
42
80
  # Fetch the next result from calling a Ruby method and prepare it for
@@ -48,8 +86,8 @@ module Oso
48
86
  # @param instance_id [Integer]
49
87
  # @raise [Error] if the FFI call raises one.
50
88
  def handle_call(method, call_id:, instance:, args:)
51
- polar.register_call(method, call_id: call_id, instance: instance, args: args)
52
- result = JSON.dump(polar.next_call_result(call_id))
89
+ register_call(method, call_id: call_id, instance: instance, args: args)
90
+ result = JSON.dump(next_call_result(call_id))
53
91
  call_result(result, call_id: call_id)
54
92
  rescue InvalidCallError, StopIteration
55
93
  call_result(nil, call_id: call_id)
@@ -65,19 +103,19 @@ module Oso
65
103
  def start # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
66
104
  Enumerator.new do |yielder| # rubocop:disable Metrics/BlockLength
67
105
  loop do # rubocop:disable Metrics/BlockLength
68
- event = ffi_instance.next_event
106
+ event = ffi_query.next_event
69
107
  case event.kind
70
108
  when 'Done'
71
109
  break
72
110
  when 'Result'
73
- yielder << event.data['bindings'].transform_values { |v| polar.to_ruby(v) }
111
+ yielder << event.data['bindings'].transform_values { |v| host.to_ruby(v) }
74
112
  when 'MakeExternal'
75
113
  id = event.data['instance_id']
76
- raise DuplicateInstanceRegistrationError, id if polar.instance? id
114
+ raise DuplicateInstanceRegistrationError, id if host.instance? id
77
115
 
78
116
  cls_name = event.data['instance']['tag']
79
117
  fields = event.data['instance']['fields']['fields']
80
- polar.make_instance(cls_name, fields: fields, id: id)
118
+ host.make_instance(cls_name, fields: fields, id: id)
81
119
  when 'ExternalCall'
82
120
  call_id = event.data['call_id']
83
121
  instance = event.data['instance']
@@ -88,24 +126,24 @@ module Oso
88
126
  instance_id = event.data['instance_id']
89
127
  left_tag = event.data['left_class_tag']
90
128
  right_tag = event.data['right_class_tag']
91
- answer = polar.subspecializer?(instance_id, left_tag: left_tag, right_tag: right_tag)
129
+ answer = host.subspecializer?(instance_id, left_tag: left_tag, right_tag: right_tag)
92
130
  question_result(answer, call_id: event.data['call_id'])
93
131
  when 'ExternalIsa'
94
132
  instance_id = event.data['instance_id']
95
133
  class_tag = event.data['class_tag']
96
- answer = polar.isa?(instance_id, class_tag: class_tag)
134
+ answer = host.isa?(instance_id, class_tag: class_tag)
97
135
  question_result(answer, call_id: event.data['call_id'])
98
136
  when 'ExternalUnify'
99
137
  left_instance_id = event.data['left_instance_id']
100
138
  right_instance_id = event.data['right_instance_id']
101
- answer = polar.unify?(left_instance_id, right_instance_id)
139
+ answer = host.unify?(left_instance_id, right_instance_id)
102
140
  question_result(answer, call_id: event.data['call_id'])
103
141
  when 'Debug'
104
142
  puts event.data['message'] if event.data['message']
105
143
  print '> '
106
144
  input = $stdin.gets.chomp!
107
- command = JSON.dump(polar.to_polar_term(input))
108
- ffi_instance.debug_command(command)
145
+ command = JSON.dump(host.to_polar_term(input))
146
+ ffi_query.debug_command(command)
109
147
  else
110
148
  raise "Unhandled event: #{JSON.dump(event.inspect)}"
111
149
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oso
4
- VERSION = '0.2.5'
4
+ VERSION = '0.2.6'
5
5
  end
@@ -18,11 +18,11 @@ Gem::Specification.new do |spec|
18
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
20
  files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
- files += Dir['ext/oso-oso/lib/*']
21
+ files + Dir['ext/oso-oso/lib/*']
22
22
  end
23
23
 
24
- spec.bindir = 'exe'
25
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.bindir = 'bin'
25
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ['lib']
27
27
 
28
28
  # Runtime dependencies
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.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oso Security
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-21 00:00:00.000000000 Z
11
+ date: 2020-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -97,7 +97,8 @@ dependencies:
97
97
  description:
98
98
  email:
99
99
  - support@osohq.com
100
- executables: []
100
+ executables:
101
+ - oso
101
102
  extensions: []
102
103
  extra_rdoc_files: []
103
104
  files:
@@ -109,8 +110,7 @@ files:
109
110
  - Makefile
110
111
  - README.md
111
112
  - Rakefile
112
- - bin/console
113
- - bin/setup
113
+ - bin/oso
114
114
  - ext/oso-oso/lib/libpolar.dylib
115
115
  - ext/oso-oso/lib/libpolar.so
116
116
  - ext/oso-oso/lib/polar.dll
@@ -125,6 +125,7 @@ files:
125
125
  - lib/oso/polar/ffi/polar.rb
126
126
  - lib/oso/polar/ffi/query.rb
127
127
  - lib/oso/polar/ffi/query_event.rb
128
+ - lib/oso/polar/host.rb
128
129
  - lib/oso/polar/polar.rb
129
130
  - lib/oso/polar/predicate.rb
130
131
  - lib/oso/polar/query.rb
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'bundler/setup'
5
- require 'oso'
6
-
7
- polar = Oso::Polar.new.tap do |p| # rubocop:disable Lint/UselessAssignment
8
- p.load_str('f(1); f(2); g(1); g(2); h(2); k(x) := f(x), h(x), g(x);')
9
- puts 'f(x)', p.send(:query_str, 'f(x)').to_a
10
- puts 'k(x)', p.send(:query_str, 'k(x)').to_a
11
-
12
- p.load_str('foo(1, 2); foo(3, 4); foo(5, 6);')
13
- expected = [{ 'x' => 1, 'y' => 2 }, { 'x' => 3, 'y' => 4 }, { 'x' => 5, 'y' => 6 }]
14
- raise 'AssertionError' if p.send(:query_str, 'foo(x, y)').to_a != expected
15
-
16
- class TestClass # rubocop:disable Style/Documentation
17
- def my_method
18
- 1
19
- end
20
- end
21
-
22
- p.register_class(TestClass)
23
-
24
- p.load_str('external(x, 3) := x = new TestClass{}.my_method;')
25
- results = p.send(:query_str, 'external(1, x)')
26
- p results.next
27
-
28
- # p.load_str('testDebug() := debug(), foo(x, y), k(y);')
29
- # p.send(:query_str, 'testDebug()').next
30
- end
31
-
32
- require 'pry'
33
- Pry.start
data/bin/setup DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install