oso-oso 0.13.1 → 0.14.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: ab6ed22597c52c4a77ff24aa6455492ba9cf7ed7
4
- data.tar.gz: 3aaa0186592b86e63d9a2dcb2976f03154e0dcda
3
+ metadata.gz: e2aee8d8308beb5cb906dff373ef43f8a90c5d3f
4
+ data.tar.gz: a121f6f4b7534f53934f49aab4e634cbbbe7f0db
5
5
  SHA512:
6
- metadata.gz: 9e7ce6bbf44da47f40c4cc5b53fab5d17870125dd5eed7443be858d9f8e0adb98fcce7799eb7fb404c867d3e10fdb5522b52d69ebc5ac6ffa280f11ad56a9623
7
- data.tar.gz: d032a1742948a1f21f8fd19576aaf99479ef534a2247bac4e7a13e9f2910292a0b74eed56cef67deece467e2ae85ba6dc31df6b3a0d2761618703647895ae19a
6
+ metadata.gz: 6cb9212a01eaa50005a5d3923632741cfbbfff8c28c42a3c7f30b2cf6bf6aeba58fbe4f4c87ff3ccb43deb21dfd2db6e3f95ce7aded5996fe2eb6cb74d5de162
7
+ data.tar.gz: ef8ec9735a3aba0c18d9da63ea0fda8b36e1d6c6cd3306611b1b126d6587f1381b2e558680058c4c926ed9d736b7fa21c96f92ae179b0df485d24ea3bd1c1791
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.13.1)
4
+ oso-oso (0.14.0)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
Binary file
Binary file
Binary file
data/lib/oso/polar.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'oso/polar/errors'
4
+ require 'oso/polar/expression'
4
5
  require 'oso/polar/ffi'
5
6
  require 'oso/polar/host'
7
+ require 'oso/polar/pattern'
6
8
  require 'oso/polar/polar'
7
9
  require 'oso/polar/predicate'
8
10
  require 'oso/polar/query'
@@ -100,8 +100,11 @@ module Oso
100
100
  class ApiError < Error; end
101
101
  class ParameterError < ApiError; end
102
102
 
103
+ class ValidationError < Error; end
104
+ class RolesValidationError < Error; end
105
+
103
106
  UNEXPECTED_EXPRESSION_MESSAGE = <<~MSG
104
- Recieved Expression from Polar VM. The Expression type is not yet supported in this language.
107
+ Received Expression from Polar VM. The Expression type is not yet supported in this language.
105
108
 
106
109
  This may mean you performed an operation in your policy over an unbound variable.
107
110
  MSG
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Polar expression.
6
+ class Expression
7
+ attr_reader :operator, :args
8
+
9
+ # @param operator [String]
10
+ # @param args [Array<Object>]
11
+ def initialize(operator, args)
12
+ @operator = operator
13
+ @args = args
14
+ end
15
+
16
+ # @param other [Expression]
17
+ # @return [Boolean]
18
+ def ==(other)
19
+ operator == other.operator && args == other.args
20
+ end
21
+
22
+ # @see #==
23
+ alias eql? ==
24
+ end
25
+ end
26
+ end
@@ -24,14 +24,22 @@ module Oso
24
24
  #
25
25
  # @return [::Oso::Polar::Error] if there's an FFI error.
26
26
  # @return [::Oso::Polar::FFIErrorNotFound] if there isn't one.
27
- def self.get # rubocop:disable Metrics/MethodLength
27
+ def self.get # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
28
28
  error = Rust.get
29
29
  return ::Oso::Polar::FFIErrorNotFound if error.null?
30
30
 
31
31
  error = JSON.parse(error.to_s)
32
32
  msg = error['formatted']
33
33
  kind, body = error['kind'].first
34
- subkind, details = body.first
34
+
35
+ # Not all errors have subkind and details.
36
+ # TODO (gj): This bug may exist in other libraries.
37
+ if body.is_a? Hash
38
+ subkind, details = body.first
39
+ else
40
+ subkind, details = nil
41
+ end
42
+
35
43
  case kind
36
44
  when 'Parse'
37
45
  parse_error(subkind, msg: msg, details: details)
@@ -41,6 +49,8 @@ module Oso
41
49
  operational_error(subkind, msg: msg, details: details)
42
50
  when 'Parameter'
43
51
  api_error(subkind, msg: msg, details: details)
52
+ when 'RolesValidation'
53
+ validation_error(msg, details: details)
44
54
  end
45
55
  end
46
56
 
@@ -121,6 +131,16 @@ module Oso
121
131
  ::Oso::Polar::ApiError.new(msg, details: details)
122
132
  end
123
133
  end
134
+
135
+ # Map FFI Validation errors into Ruby exceptions.
136
+ #
137
+ # @param msg [String]
138
+ # @param details [Hash<String, Object>]
139
+ # @return [::Oso::Polar::ValidationError] the object converted into the expected format.
140
+ private_class_method def self.validation_error(msg, details:)
141
+ # This is currently the only type of validation error.
142
+ ::Oso::Polar::RolesValidationError.new(msg, details: details)
143
+ end
124
144
  end
125
145
  end
126
146
  end
@@ -12,6 +12,8 @@ module Oso
12
12
  ffi_lib FFI::LIB_PATH
13
13
 
14
14
  attach_function :new, :polar_new, [], FFI::Polar
15
+ attach_function :enable_roles, :polar_enable_roles, [FFI::Polar], :int32
16
+ attach_function :validate_roles_config, :polar_validate_roles_config, [FFI::Polar, :string], :int32
15
17
  attach_function :load, :polar_load, [FFI::Polar, :string, :string], :int32
16
18
  attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
17
19
  attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
@@ -33,6 +35,20 @@ module Oso
33
35
  polar
34
36
  end
35
37
 
38
+ # @raise [FFI::Error] if the FFI call returns an error.
39
+ def enable_roles
40
+ result = Rust.enable_roles(self)
41
+ process_messages
42
+ raise FFI::Error.get if result.zero?
43
+ end
44
+
45
+ # @raise [FFI::Error] if the FFI call returns an error.
46
+ def validate_roles_config(config)
47
+ result = Rust.validate_roles_config(self, JSON.dump(config))
48
+ process_messages
49
+ raise FFI::Error.get if result.zero?
50
+ end
51
+
36
52
  # @param src [String]
37
53
  # @param filename [String]
38
54
  # @raise [FFI::Error] if the FFI call returns an error.
@@ -2,6 +2,38 @@
2
2
 
3
3
  module Oso
4
4
  module Polar
5
+ # Ruby code reloaders (i.e. the one used by rails) swap out the value of
6
+ # a constant on code changes. Because of this, we can't reliably call
7
+ # `is_a?` on the constant that was passed to `register_class`.
8
+ #
9
+ # Example (where Foo is a class defined in foo.rb):
10
+ # > klass = Foo
11
+ # > Foo.new.is_a? klass
12
+ # => true
13
+ # > ... user changes foo.rb ...
14
+ # > Foo.new.is_a? klass
15
+ # => false
16
+ #
17
+ # To solve this, when we need to access the class (e.g. during isa), we
18
+ # look it up using const_get, which will always return the up-to-date
19
+ # version of the class.
20
+ class PolarClass
21
+ attr_reader :name, :anon_class
22
+
23
+ def initialize(klass)
24
+ @name = klass.name
25
+ # If the class doesn't have a name, it is anonymous, meaning we should
26
+ # actually store it directly
27
+ @anon_class = klass if klass.name.nil?
28
+ end
29
+
30
+ def get
31
+ return anon_class if anon_class
32
+
33
+ Object.const_get(name)
34
+ end
35
+ end
36
+
5
37
  # Translate between Polar and the host language (Ruby).
6
38
  class Host # rubocop:disable Metrics/ClassLength
7
39
  protected
@@ -12,13 +44,18 @@ module Oso
12
44
  attr_reader :classes
13
45
  # @return [Hash<Integer, Object>]
14
46
  attr_reader :instances
47
+ # @return [Boolean]
48
+ attr_reader :accept_expression
15
49
 
16
50
  public
17
51
 
52
+ attr_writer :accept_expression
53
+
18
54
  def initialize(ffi_polar)
19
55
  @ffi_polar = ffi_polar
20
56
  @classes = {}
21
57
  @instances = {}
58
+ @accept_expression = false
22
59
  end
23
60
 
24
61
  def initialize_copy(other)
@@ -35,7 +72,7 @@ module Oso
35
72
  def get_class(name)
36
73
  raise UnregisteredClassError, name unless classes.key? name
37
74
 
38
- classes[name]
75
+ classes[name].get
39
76
  end
40
77
 
41
78
  # Store a Ruby class in the {#classes} cache.
@@ -48,7 +85,7 @@ module Oso
48
85
  def cache_class(cls, name:)
49
86
  raise DuplicateClassAliasError.new name: name, old: get_class(name), new: cls if classes.key? name
50
87
 
51
- classes[name] = cls
88
+ classes[name] = PolarClass.new(cls)
52
89
  name
53
90
  end
54
91
 
@@ -73,7 +110,10 @@ module Oso
73
110
  def get_instance(id)
74
111
  raise UnregisteredInstanceError, id unless instance? id
75
112
 
76
- instances[id]
113
+ instance = instances[id]
114
+ return instance.get if instance.is_a? PolarClass
115
+
116
+ instance
77
117
  end
78
118
 
79
119
  # Cache a Ruby instance in the {#instances} cache, fetching a new id if
@@ -84,6 +124,8 @@ module Oso
84
124
  # @return [Integer] the instance ID.
85
125
  def cache_instance(instance, id: nil)
86
126
  id = ffi_polar.new_id if id.nil?
127
+ # Save the instance as a PolarClass if it is a non-anonymous class
128
+ instance = PolarClass.new(instance) if instance.is_a?(Class)
87
129
  instances[id] = instance
88
130
  id
89
131
  end
@@ -173,7 +215,16 @@ module Oso
173
215
  { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar(el) } } }
174
216
  when value.instance_of?(Variable)
175
217
  # This is supported so that we can query for unbound variables
176
- { 'Variable' => value }
218
+ { 'Variable' => value.name }
219
+ when value.instance_of?(Expression)
220
+ { 'Expression' => { 'operator' => value.operator, 'args' => value.args.map { |el| to_polar(el) } } }
221
+ when value.instance_of?(Pattern)
222
+ dict = to_polar(value.fields)['value']
223
+ if value.tag.nil?
224
+ { 'Pattern' => dict }
225
+ else
226
+ { 'Pattern' => { 'Instance' => { 'tag' => value.tag, 'fields' => dict['Dictionary'] } } }
227
+ end
177
228
  else
178
229
  { 'ExternalInstance' => { 'instance_id' => cache_instance(value), 'repr' => value.to_s } }
179
230
  end
@@ -219,7 +270,24 @@ module Oso
219
270
  when 'Call'
220
271
  Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
221
272
  when 'Variable'
222
- Variable.new(value['name'])
273
+ Variable.new(value)
274
+ when 'Expression'
275
+ raise UnexpectedPolarTypeError, tag unless accept_expression
276
+
277
+ args = value['args'].map { |a| to_ruby(a) }
278
+ Expression.new(value['operator'], args)
279
+ when 'Pattern'
280
+ case value.keys.first
281
+ when 'Instance'
282
+ tag = value.values.first['tag']
283
+ fields = value.values.first['fields']['fields'].transform_values { |v| to_ruby(v) }
284
+ Pattern.new(tag, fields)
285
+ when 'Dictionary'
286
+ fields = value.values.first['fields'].transform_values { |v| to_ruby(v) }
287
+ Pattern.new(nil, fields)
288
+ else
289
+ raise UnexpectedPolarTypeError, "#{value.keys.first} variant of Pattern"
290
+ end
223
291
  else
224
292
  raise UnexpectedPolarTypeError, tag
225
293
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Polar pattern.
6
+ class Pattern
7
+ attr_reader :tag, :fields
8
+
9
+ # @param tag [String]
10
+ # @param fields [Hash<String, Object>]
11
+ def initialize(tag, fields)
12
+ @tag = tag
13
+ @fields = fields
14
+ end
15
+
16
+ # @param other [Pattern]
17
+ # @return [Boolean]
18
+ def ==(other)
19
+ tag == other.tag && fields == other.fields
20
+ end
21
+
22
+ # @see #==
23
+ alias eql? ==
24
+ end
25
+ end
26
+ end
@@ -37,6 +37,7 @@ module Oso
37
37
  def initialize
38
38
  @ffi_polar = FFI::Polar.create
39
39
  @host = Host.new(ffi_polar)
40
+ @polar_roles_enabled = false
40
41
 
41
42
  # Register global constants.
42
43
  register_constant nil, name: 'nil'
@@ -50,11 +51,48 @@ module Oso
50
51
  register_class String
51
52
  end
52
53
 
54
+ def enable_roles # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
55
+ return if polar_roles_enabled
56
+
57
+ roles_helper = Class.new do
58
+ def self.join(separator, left, right)
59
+ [left, right].join(separator)
60
+ end
61
+ end
62
+ register_constant(roles_helper, name: '__oso_internal_roles_helpers__')
63
+ ffi_polar.enable_roles
64
+ self.polar_roles_enabled = true
65
+
66
+ # validate config
67
+ validation_query_results = []
68
+ loop do
69
+ query = ffi_polar.next_inline_query
70
+ break if query.nil?
71
+
72
+ new_host = host.dup
73
+ new_host.accept_expression = true
74
+ results = Query.new(query, host: new_host).to_a
75
+ raise InlineQueryFailedError, query.source if results.empty?
76
+
77
+ validation_query_results.push results
78
+ end
79
+
80
+ # turn bindings back into polar
81
+ validation_query_results = validation_query_results.map do |results|
82
+ results.map do |result|
83
+ { 'bindings' => result.transform_values { |v| host.to_polar(v) } }
84
+ end
85
+ end
86
+
87
+ ffi_polar.validate_roles_config(validation_query_results)
88
+ end
89
+
53
90
  # Clear all rules and rule sources from the current Polar instance
54
91
  #
55
92
  # @return [self] for chaining.
56
93
  def clear_rules
57
94
  ffi_polar.clear_rules
95
+ ffi_polar.enable_roles if polar_roles_enabled
58
96
  self
59
97
  end
60
98
 
@@ -81,7 +119,7 @@ module Oso
81
119
  # @raise [InlineQueryFailedError] on the first failed inline query.
82
120
  # @raise [Error] if any of the FFI calls raise one.
83
121
  # @return [self] for chaining.
84
- def load_str(str, filename: nil)
122
+ def load_str(str, filename: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
85
123
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
86
124
 
87
125
  ffi_polar.load(str, filename: filename)
@@ -91,6 +129,13 @@ module Oso
91
129
 
92
130
  raise InlineQueryFailedError, next_query.source if Query.new(next_query, host: host).first.nil?
93
131
  end
132
+
133
+ # If roles are enabled, re-validate config when new rules are loaded.
134
+ if polar_roles_enabled
135
+ self.polar_roles_enabled = false
136
+ enable_roles
137
+ end
138
+
94
139
  self
95
140
  end
96
141
 
@@ -170,6 +215,7 @@ module Oso
170
215
 
171
216
  # @return [FFI::Polar]
172
217
  attr_reader :ffi_polar
218
+ attr_accessor :polar_roles_enabled
173
219
 
174
220
  # The R and L in REPL for systems where readline is available.
175
221
  def repl_readline(prompt)
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.13.1'
4
+ VERSION = '0.14.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.13.1
4
+ version: 0.14.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-06-30 00:00:00.000000000 Z
11
+ date: 2021-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -133,6 +133,7 @@ files:
133
133
  - lib/oso/oso.rb
134
134
  - lib/oso/polar.rb
135
135
  - lib/oso/polar/errors.rb
136
+ - lib/oso/polar/expression.rb
136
137
  - lib/oso/polar/ffi.rb
137
138
  - lib/oso/polar/ffi/error.rb
138
139
  - lib/oso/polar/ffi/message.rb
@@ -141,6 +142,7 @@ files:
141
142
  - lib/oso/polar/ffi/query_event.rb
142
143
  - lib/oso/polar/ffi/source.rb
143
144
  - lib/oso/polar/host.rb
145
+ - lib/oso/polar/pattern.rb
144
146
  - lib/oso/polar/polar.rb
145
147
  - lib/oso/polar/predicate.rb
146
148
  - lib/oso/polar/query.rb