oso-cloud 0.7.0 → 1.0.1

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
  SHA256:
3
- metadata.gz: 69149698f1269f5bd98bf3072671992b364170e0ff5264132fde524f10d997b2
4
- data.tar.gz: ccd52aa2087dfadffc11dce6343aaad7abf10ec3cb2099a8b01df8f19f3e88d4
3
+ metadata.gz: 339079e696596a482f6f1fe6bc875d9f88dc5415b0eaca4b4e7a04304280cc7a
4
+ data.tar.gz: '099c6b0532405a7fc0cf5933b09ea03991fe43f3e2da6b726d5e33e932706b55'
5
5
  SHA512:
6
- metadata.gz: 2cc987575b2ef6ff3b7bdff48b575c23f1a6739ba5553a44f00731700e03b89a854d71bac3fd60b41bf76854d9b38fb9b380ee2eb7b4abdc7a0338abcb48b3ed
7
- data.tar.gz: afa3922a254311adf4616372c15b3c51c834339d89c5cc3942c86d261e12121d919b8879dd9494e28cb71b7f318f060924e1852cae28e36e7fcf385c70591cb1
6
+ metadata.gz: 441e11c7fdb4b201cf22d84195d7f0cc5454a64186c0078086ae085138a31a8ee10f667f5723b94b467aa74ee89780e1ccd853c435d044f84616b46f78a44527
7
+ data.tar.gz: 56c4cb7d88820805bbd9238624f8220253c48aa19fdffe71298af26ff9369e3bf5692d49be70f439a582e4f76ff9d4b7458f45a303fa32c02296c5c2324f9f08
data/Gemfile.lock CHANGED
@@ -1,13 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-cloud (0.7.0)
4
+ oso-cloud (1.0.1)
5
+ faraday (~> 2.5.2)
6
+ faraday-retry (~> 2.0.0)
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
8
10
  specs:
11
+ faraday (2.5.2)
12
+ faraday-net_http (>= 2.0, < 3.1)
13
+ ruby2_keywords (>= 0.0.4)
14
+ faraday-net_http (3.0.2)
15
+ faraday-retry (2.0.0)
16
+ faraday (~> 2.0)
9
17
  minitest (5.15.0)
10
18
  rake (12.3.3)
19
+ ruby2_keywords (0.0.5)
11
20
 
12
21
  PLATFORMS
13
22
  ruby
data/README.md CHANGED
@@ -1,25 +1,53 @@
1
- # Oso::Client
1
+ # Oso Cloud Client for Ruby
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/oso/client`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [![Slack][badge-slack]][badge-slack-link]
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ The Oso Cloud client for Ruby provides a convenient wrapper around the Oso
6
+ Cloud HTTP API for applications and services written in Ruby.
6
7
 
7
- ## Installation
8
+ ## What is Oso Cloud?
9
+ Oso Cloud is authorization-as-a-service. It provides abstractions for building
10
+ and iterating on authorization in your application – based on years of work
11
+ with hundreds of engineering teams.
8
12
 
9
- Add this line to your application's Gemfile:
13
+ - Model: Build your authorization model using primitives for common patterns
14
+ like multi-tenancy and RBAC. Express custom rules using Polar, a
15
+ declarative policy language for authorization.
10
16
 
11
- ```ruby
12
- gem 'oso-cloud'
13
- ```
17
+ - Store: Store your authorization data using a best-practices data model and
18
+ use it for access decisions across all of your services.
14
19
 
15
- And then execute:
20
+ - Enforce & Query: Add enforcement calls to your application to perform
21
+ yes/no permission checks, filter resources by permissions, list a user's
22
+ roles, and show/hide pieces of your UI.
16
23
 
17
- $ bundle install
24
+ - Test & Watch: Write tests over your authorization policies before you push
25
+ them live. See logs of authorization decisions in real time.
18
26
 
19
- Or install it yourself as:
27
+ For more information on how Oso Cloud works and how it fits into your
28
+ architecture, check out the
29
+ [introduction](https://www.osohq.com/docs/get-started/what-is-oso-cloud).
20
30
 
21
- $ gem install oso-cloud
31
+ ## Documentation
32
+ - To get up and running with Oso Cloud, try the
33
+ [Quickstart guide](https://www.osohq.com/docs/get-started/quickstart).
34
+ - For method-level documentation, see the
35
+ [Ruby Client API documentation](https://www.osohq.com/docs/reference/client-apis/ruby).
36
+ - Full documentation is available at
37
+ [osohq.com/docs](https://www.osohq.com/docs).
38
+ - To learn about authorization best practices (not specific to Oso), read the
39
+ [Authorization Academy](https://www.osohq.com/developers/authorization-academy)
40
+ guides.
22
41
 
23
- ## Usage
42
+ ## Community & Support
43
+
44
+ If you have any questions on Oso Cloud or authorization more generally, you can
45
+ join our engineering team & hundreds of other developers using Oso in our
46
+ community Slack:
47
+
48
+ [![Button][join-slack-link]][badge-slack-link]
49
+
50
+ [join-slack-link]: https://user-images.githubusercontent.com/282595/128394344-1bd9e5b2-e83d-4666-b446-2e4f431ffcea.png
51
+ [badge-slack]: https://img.shields.io/badge/slack-oso--oss-orange
52
+ [badge-slack-link]: https://join-slack.osohq.com/
24
53
 
25
- TODO: Write usage instructions here
data/lib/oso/api.rb ADDED
@@ -0,0 +1,458 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+
6
+ require 'oso/helpers'
7
+
8
+ module OsoCloud
9
+ # @!visibility private
10
+ module Core
11
+ # @!visibility private
12
+ class ApiResult
13
+ attr_reader :message
14
+
15
+ def initialize(message:)
16
+ @message = message
17
+ end
18
+ end
19
+
20
+ # @!visibility private
21
+ class ApiError < StandardError
22
+ def initialize(message:)
23
+ super(message)
24
+ end
25
+ end
26
+
27
+ # @!visibility private
28
+ class Policy
29
+ attr_reader :filename
30
+ attr_reader :src
31
+
32
+ def initialize(filename:, src:)
33
+ @filename = filename
34
+ @src = src
35
+ end
36
+ end
37
+
38
+ # @!visibility private
39
+ class GetPolicyResult
40
+ attr_reader :policy
41
+
42
+ def initialize(policy:)
43
+ if policy.is_a? Policy
44
+ @policy = policy
45
+ else
46
+ @policy = Policy.new(**policy)
47
+ end
48
+ end
49
+ end
50
+
51
+ # @!visibility private
52
+ class Fact
53
+ attr_reader :predicate
54
+ attr_reader :args
55
+
56
+ def initialize(predicate:, args:)
57
+ @predicate = predicate
58
+ @args = args.map { |v| if v.is_a? Value then v else Value.new(**v) end }
59
+ end
60
+ end
61
+
62
+ # @!visibility private
63
+ class Value
64
+ attr_reader :type
65
+ attr_reader :id
66
+
67
+ def initialize(type:, id:)
68
+ @type = type
69
+ @id = id
70
+ end
71
+ end
72
+
73
+ # @!visibility private
74
+ class Bulk
75
+ attr_reader :delete
76
+ attr_reader :tell
77
+
78
+ def initialize(delete:, tell:)
79
+ @delete = delete.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
80
+ @tell = tell.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
81
+ end
82
+ end
83
+
84
+ # @!visibility private
85
+ class AuthorizeResult
86
+ attr_reader :allowed
87
+
88
+ def initialize(allowed:)
89
+ @allowed = allowed
90
+ end
91
+ end
92
+
93
+ # @!visibility private
94
+ class AuthorizeQuery
95
+ attr_reader :actor_type
96
+ attr_reader :actor_id
97
+ attr_reader :action
98
+ attr_reader :resource_type
99
+ attr_reader :resource_id
100
+ attr_reader :context_facts
101
+
102
+ def initialize(actor_type:, actor_id:, action:, resource_type:, resource_id:, context_facts:)
103
+ @actor_type = actor_type
104
+ @actor_id = actor_id
105
+ @action = action
106
+ @resource_type = resource_type
107
+ @resource_id = resource_id
108
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
109
+ end
110
+ end
111
+
112
+ # @!visibility private
113
+ class AuthorizeResourcesResult
114
+ attr_reader :results
115
+
116
+ def initialize(results:)
117
+ @results = results.map { |v| if v.is_a? Value then v else Value.new(**v) end }
118
+ end
119
+ end
120
+
121
+ # @!visibility private
122
+ class AuthorizeResourcesQuery
123
+ attr_reader :actor_type
124
+ attr_reader :actor_id
125
+ attr_reader :action
126
+ attr_reader :resources
127
+ attr_reader :context_facts
128
+
129
+ def initialize(actor_type:, actor_id:, action:, resources:, context_facts:)
130
+ @actor_type = actor_type
131
+ @actor_id = actor_id
132
+ @action = action
133
+ @resources = resources.map { |v| if v.is_a? Value then v else Value.new(**v) end }
134
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
135
+ end
136
+ end
137
+
138
+ # @!visibility private
139
+ class ListResult
140
+ attr_reader :results
141
+
142
+ def initialize(results:)
143
+ @results = results
144
+ end
145
+ end
146
+
147
+ # @!visibility private
148
+ class ListQuery
149
+ attr_reader :actor_type
150
+ attr_reader :actor_id
151
+ attr_reader :action
152
+ attr_reader :resource_type
153
+ attr_reader :context_facts
154
+
155
+ def initialize(actor_type:, actor_id:, action:, resource_type:, context_facts:)
156
+ @actor_type = actor_type
157
+ @actor_id = actor_id
158
+ @action = action
159
+ @resource_type = resource_type
160
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
161
+ end
162
+ end
163
+
164
+ # @!visibility private
165
+ class ActionsResult
166
+ attr_reader :results
167
+
168
+ def initialize(results:)
169
+ @results = results
170
+ end
171
+ end
172
+
173
+ # @!visibility private
174
+ class ActionsQuery
175
+ attr_reader :actor_type
176
+ attr_reader :actor_id
177
+ attr_reader :resource_type
178
+ attr_reader :resource_id
179
+ attr_reader :context_facts
180
+
181
+ def initialize(actor_type:, actor_id:, resource_type:, resource_id:, context_facts:)
182
+ @actor_type = actor_type
183
+ @actor_id = actor_id
184
+ @resource_type = resource_type
185
+ @resource_id = resource_id
186
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
187
+ end
188
+ end
189
+
190
+ # @!visibility private
191
+ class QueryResult
192
+ attr_reader :results
193
+
194
+ def initialize(results:)
195
+ @results = results.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
196
+ end
197
+ end
198
+
199
+ # @!visibility private
200
+ class Query
201
+ attr_reader :fact
202
+ attr_reader :context_facts
203
+
204
+ def initialize(fact:, context_facts:)
205
+ if fact.is_a? Fact
206
+ @fact = fact
207
+ else
208
+ @fact = Fact.new(**fact)
209
+ end
210
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
211
+ end
212
+ end
213
+
214
+ # @!visibility private
215
+ class StatsResult
216
+ attr_reader :num_roles
217
+ attr_reader :num_relations
218
+ attr_reader :num_facts
219
+
220
+ def initialize(num_roles:, num_relations:, num_facts:)
221
+ @num_roles = num_roles
222
+ @num_relations = num_relations
223
+ @num_facts = num_facts
224
+ end
225
+ end
226
+
227
+
228
+ # @!visibility private
229
+ class Api
230
+ def initialize(url: 'https://cloud.osohq.com', api_key: nil, options: nil)
231
+ @url = url
232
+ @connection = Faraday.new(url: url) do |faraday|
233
+ faraday.request :json
234
+
235
+ # responses are processed in reverse order; this stack implies the
236
+ # retries are attempted before an error is raised, and the json
237
+ # parser is only applied if there are no errors
238
+ faraday.response :json, preserve_raw: true
239
+ faraday.response :raise_error
240
+ faraday.request :retry, {
241
+ max: (options && options[:max_retries]) || 10,
242
+ interval: 0.01,
243
+ interval_randomness: 0.005,
244
+ max_interval: 1,
245
+ backoff_factor: 2,
246
+ retry_statuses: [429, 500, 502, 503, 504],
247
+ # ensure authorize and related check functions are retried because
248
+ # they are POST requests, which are not retried automatically
249
+ retry_if: ->(env, _exc) {
250
+ %w[
251
+ /api/authorize
252
+ /api/authorize_resources
253
+ /api/list
254
+ /api/actions
255
+ /api/query
256
+ ].include? env.url.path
257
+ },
258
+ }
259
+
260
+ if (options && options[:test_adapter])
261
+ faraday.adapter :test do |stub|
262
+ stub.post(options[:test_adapter][:path]) do |env|
263
+ options[:test_adapter][:func].call
264
+ end
265
+ stub.get(options[:test_adapter][:path]) do |env|
266
+ options[:test_adapter][:func].call
267
+ end
268
+ stub.delete(options[:test_adapter][:path]) do |env|
269
+ options[:test_adapter][:func].call
270
+ end
271
+ end
272
+ else
273
+ faraday.adapter :net_http
274
+ end
275
+ end
276
+ @api_key = api_key
277
+ end
278
+
279
+ def get_policy()
280
+ params = {}
281
+ data = nil
282
+ url = "/policy"
283
+ result = GET(url, params, data)
284
+ GetPolicyResult.new(**result)
285
+ end
286
+
287
+ def post_policy(data)
288
+ params = {}
289
+ data = OsoCloud::Helpers.to_hash(data)
290
+ url = "/policy"
291
+ result = POST(url, params, data)
292
+ ApiResult.new(**result)
293
+ end
294
+
295
+ def post_facts(data)
296
+ params = {}
297
+ data = OsoCloud::Helpers.to_hash(data)
298
+ url = "/facts"
299
+ result = POST(url, params, data)
300
+ Fact.new(**result)
301
+ end
302
+
303
+ def delete_facts(data)
304
+ params = {}
305
+ data = OsoCloud::Helpers.to_hash(data)
306
+ url = "/facts"
307
+ result = DELETE(url, params, data)
308
+ ApiResult.new(**result)
309
+ end
310
+
311
+ def post_bulk_load(data)
312
+ params = {}
313
+ data = OsoCloud::Helpers.to_hash(data)
314
+ url = "/bulk_load"
315
+ result = POST(url, params, data)
316
+ ApiResult.new(**result)
317
+ end
318
+
319
+ def post_bulk_delete(data)
320
+ params = {}
321
+ data = OsoCloud::Helpers.to_hash(data)
322
+ url = "/bulk_delete"
323
+ result = POST(url, params, data)
324
+ ApiResult.new(**result)
325
+ end
326
+
327
+ def post_bulk(data)
328
+ params = {}
329
+ data = OsoCloud::Helpers.to_hash(data)
330
+ url = "/bulk"
331
+ result = POST(url, params, data)
332
+ ApiResult.new(**result)
333
+ end
334
+
335
+ def post_authorize(data)
336
+ params = {}
337
+ data = OsoCloud::Helpers.to_hash(data)
338
+ url = "/authorize"
339
+ result = POST(url, params, data)
340
+ AuthorizeResult.new(**result)
341
+ end
342
+
343
+ def post_authorize_resources(data)
344
+ params = {}
345
+ data = OsoCloud::Helpers.to_hash(data)
346
+ url = "/authorize_resources"
347
+ result = POST(url, params, data)
348
+ AuthorizeResourcesResult.new(**result)
349
+ end
350
+
351
+ def post_list(data)
352
+ params = {}
353
+ data = OsoCloud::Helpers.to_hash(data)
354
+ url = "/list"
355
+ result = POST(url, params, data)
356
+ ListResult.new(**result)
357
+ end
358
+
359
+ def post_actions(data)
360
+ params = {}
361
+ data = OsoCloud::Helpers.to_hash(data)
362
+ url = "/actions"
363
+ result = POST(url, params, data)
364
+ ActionsResult.new(**result)
365
+ end
366
+
367
+ def post_query(data)
368
+ params = {}
369
+ data = OsoCloud::Helpers.to_hash(data)
370
+ url = "/query"
371
+ result = POST(url, params, data)
372
+ QueryResult.new(**result)
373
+ end
374
+
375
+ def get_stats()
376
+ params = {}
377
+ data = nil
378
+ url = "/stats"
379
+ result = GET(url, params, data)
380
+ StatsResult.new(**result)
381
+ end
382
+
383
+ def clear_data()
384
+ params = {}
385
+ data = nil
386
+ url = "/clear_data"
387
+ result = POST(url, params, data)
388
+ ApiResult.new(**result)
389
+ end
390
+
391
+
392
+ # hard-coded, not generated
393
+ def get_facts(predicate, args)
394
+ params = {}
395
+ params["predicate"] = predicate
396
+ args.each_with_index do |arg, i|
397
+ arg_query = OsoCloud::Helpers.extract_arg_query(arg)
398
+ if arg_query
399
+ params["args.#{i}.type"] = arg_query.type
400
+ params["args.#{i}.id"] = arg_query.id
401
+ end
402
+ end
403
+ data = nil
404
+ url = "/facts"
405
+ result = GET(url, params, data)
406
+ result.map { |v| Fact.new(**v) }
407
+ end
408
+
409
+ def headers()
410
+ {
411
+ "Authorization" => "Bearer %s" % @api_key,
412
+ "User-Agent" => "Oso Cloud (ruby)",
413
+ "Accept": "application/json",
414
+ "Content-Type": "application/json",
415
+ "X-OsoApiVersion": "0"
416
+ }
417
+ end
418
+
419
+ def GET(path, params, body)
420
+ response = @connection.get("api#{path}", params, headers )
421
+ handle_faraday_response response
422
+ rescue Faraday::Error => error
423
+ handle_faraday_error error
424
+ end
425
+
426
+ def POST(path, params, body)
427
+ response = @connection.post("api#{path}", body, headers) do |req|
428
+ req.params = params
429
+ end
430
+ handle_faraday_response response
431
+ rescue Faraday::Error => error
432
+ handle_faraday_error error
433
+ end
434
+
435
+ def DELETE(path, params, body)
436
+ response = @connection.delete("api#{path}", params, headers) do |req|
437
+ req.body = body
438
+ end
439
+ handle_faraday_response response
440
+ rescue Faraday::Error => error
441
+ handle_faraday_error error
442
+ end
443
+
444
+ def handle_faraday_response(response)
445
+ # TODO:(@patrickod) refactor duplicative JSON parsing
446
+ JSON.parse(response.env[:raw_body], symbolize_names: true)
447
+ end
448
+
449
+ def handle_faraday_error(error)
450
+ err = JSON.parse(error.response[:body], symbolize_names: true)
451
+ raise ApiError.new(**err)
452
+ rescue JSON::ParserError => e
453
+ raise ApiError.new(message: e.message)
454
+ end
455
+ end
456
+
457
+ end
458
+ end
@@ -0,0 +1,59 @@
1
+ module OsoCloud
2
+ # @!visibility private
3
+ module Helpers
4
+ # @!visibility private
5
+ def self.extract_value(x)
6
+ return OsoCloud::Core::Value.new(type: "String", id: x) if x.is_a? String
7
+
8
+ return nil if x.nil?
9
+
10
+ type = (x.type.nil? ? nil : x.type.to_s)
11
+ id = (x.id.nil? ? nil : x.id.to_s)
12
+ OsoCloud::Core::Value.new(type: type, id: id)
13
+ end
14
+
15
+ # @!visibility private
16
+ def self.extract_arg_query(x)
17
+ self.extract_value(x)
18
+ end
19
+
20
+ # @!visibility private
21
+ def self.param_to_fact(predicate, args)
22
+ OsoCloud::Core::Fact.new(predicate: predicate, args: args.map { |a| self.extract_value(a) })
23
+ end
24
+
25
+ # @!visibility private
26
+ def self.params_to_facts(facts)
27
+ facts.map { |predicate, *args| self.param_to_fact(predicate, args) }
28
+ end
29
+
30
+ def self.from_value(value)
31
+ if value.id.nil?
32
+ if value.type.nil?
33
+ nil
34
+ else
35
+ { type: value.type }
36
+ end
37
+ else
38
+ if value.type == "String"
39
+ value.id
40
+ else
41
+ { id: value.id, type: value.type }
42
+ end
43
+ end
44
+ end
45
+
46
+ # @!visibility private
47
+ def self.to_hash(o)
48
+ return o.map { |v| self.to_hash(v) } if o.is_a? Array
49
+ return o if o.instance_variables.empty?
50
+ hash = {}
51
+ o.instance_variables.each { |var|
52
+ v = var.to_s.delete("@")
53
+ value = o.send(v)
54
+ hash[v] = self.to_hash(value)
55
+ }
56
+ hash
57
+ end
58
+ end
59
+ end
data/lib/oso/oso.rb ADDED
@@ -0,0 +1,255 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ require 'oso/version'
6
+ require 'oso/api'
7
+ require 'oso/helpers'
8
+
9
+ ##
10
+ # For more detailed documentation, see
11
+ # https://www.osohq.com/docs/reference/client-apis/ruby
12
+ module OsoCloud
13
+
14
+ # Represents an object in your application, with a type and id.
15
+ # Both "type" and "id" should be strings.
16
+ Value = Struct::new(:type, :id, keyword_init: true) do
17
+
18
+ def to_api_value
19
+ OsoCloud::Helpers.extract_value(self)
20
+ end
21
+ end
22
+
23
+ # Oso Cloud client for Ruby
24
+ #
25
+ # About facts:
26
+ #
27
+ # Some of these methods accept and return "fact"s.
28
+ # A "fact" is an array with at least one element.
29
+ # The first element must be a string, representing the fact's name.
30
+ # Any other elements in the array, which together represent the fact's arguments,
31
+ # can be "OsoCloud::Value" objects or strings.
32
+ class Oso
33
+ def initialize(url: 'https://cloud.osohq.com', api_key: nil)
34
+ @api = OsoCloud::Core::Api.new(url: url, api_key: api_key)
35
+ end
36
+
37
+ ##
38
+ # Update the active policy
39
+ #
40
+ # Updates the active policy in Oso Cloud, The string passed into
41
+ # this method should be written in Polar.
42
+ #
43
+ # @param policy [String]
44
+ # @return [nil]
45
+ def policy(policy)
46
+ @api.post_policy(OsoCloud::Core::Policy.new(src: policy, filename: ""))
47
+ nil
48
+ end
49
+
50
+ ##
51
+ # Check a permission
52
+ #
53
+ # Returns true if the actor can perform the action on the resource;
54
+ # otherwise false.
55
+ #
56
+ # @param actor [OsoCloud::Value]
57
+ # @param action [String]
58
+ # @param resource [OsoCloud::Value]
59
+ # @param context_facts [Array<fact>]
60
+ # @return [Boolean]
61
+ # @see Oso more information about facts
62
+ def authorize(actor, action, resource, context_facts = [])
63
+ actor_typed_id = actor.to_api_value
64
+ resource_typed_id = resource.to_api_value
65
+ result = @api.post_authorize(OsoCloud::Core::AuthorizeQuery.new(
66
+ actor_type: actor_typed_id.type,
67
+ actor_id: actor_typed_id.id,
68
+ action: action,
69
+ resource_type: resource_typed_id.type,
70
+ resource_id: resource_typed_id.id,
71
+ context_facts: OsoCloud::Helpers.params_to_facts(context_facts)
72
+ ))
73
+ result.allowed
74
+ end
75
+
76
+ ##
77
+ # Check authorized resources
78
+ #
79
+ # Returns a subset of the resource which an actor can perform
80
+ # a particular action. Ordering and duplicates, if any exist, are preserved.
81
+ #
82
+ # @param actor [OsoCloud::Value]
83
+ # @param action [String]
84
+ # @param resources [Array<OsoCloud::Value>]
85
+ # @param context_facts [Array<fact>]
86
+ # @return [Array<OsoCloud::Value>]
87
+ # @see Oso more information about facts
88
+ def authorize_resources(actor, action, resources, context_facts = [])
89
+ return [] if resources.nil?
90
+ return [] if resources.empty?
91
+
92
+ key = lambda do |type, id|
93
+ "#{type}:#{id}"
94
+ end
95
+
96
+ resources_extracted = resources.map(&:to_api_value)
97
+ actor_typed_id = actor.to_api_value
98
+ data = OsoCloud::Core::AuthorizeResourcesQuery.new(
99
+ actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
100
+ action: action,
101
+ resources: resources_extracted,
102
+ context_facts: OsoCloud::Helpers::params_to_facts(context_facts)
103
+ )
104
+ result = @api.post_authorize_resources(data)
105
+
106
+ return [] if result.results.empty?
107
+
108
+ results_lookup = Hash.new
109
+ result.results.each do |r|
110
+ k = key.call(r.type, r.id)
111
+ if results_lookup[k] == nil
112
+ results_lookup[k] = true
113
+ end
114
+ end
115
+
116
+ results = resources.select do |r|
117
+ e = r.to_api_value
118
+ exists = results_lookup[key.call(e.type, e.id)]
119
+ exists
120
+ end
121
+ results
122
+ end
123
+
124
+ ##
125
+ # List authorized resources
126
+ #
127
+ # Fetches a list of resource ids on which an actor can perform a
128
+ # particular action.
129
+ #
130
+ # @param actor [OsoCloud::Value]
131
+ # @param action [String]
132
+ # @param resource_type [String]
133
+ # @param context_facts [Array<fact>]
134
+ # @return [Array<String>]
135
+ # @see Oso more information about facts
136
+ def list(actor, action, resource_type, context_facts = [])
137
+ actor_typed_id = actor.to_api_value
138
+ result = @api.post_list(OsoCloud::Core::ListQuery.new(
139
+ actor_type: actor_typed_id.type,
140
+ actor_id: actor_typed_id.id,
141
+ action: action,
142
+ resource_type: resource_type,
143
+ context_facts: OsoCloud::Helpers.params_to_facts(context_facts)
144
+ ))
145
+ result.results
146
+ end
147
+
148
+ ##
149
+ # List authorized actions
150
+ #
151
+ # Fetches a list of actions which an actor can perform on a particular resource.
152
+ #
153
+ # @param actor [OsoCloud::Value]
154
+ # @param resource [OsoCloud::Value]
155
+ # @param context_facts [Array<fact>]
156
+ # @return [Array<String>]
157
+ # @see Oso more information about facts
158
+ def actions(actor, resource, context_facts = [])
159
+ actor_typed_id = actor.to_api_value
160
+ resource_typed_id = resource.to_api_value
161
+ result = @api.post_actions(OsoCloud::Core::ActionsQuery.new(
162
+ actor_type: actor_typed_id.type,
163
+ actor_id: actor_typed_id.id,
164
+ resource_type: resource_typed_id.type,
165
+ resource_id: resource_typed_id.id,
166
+ context_facts: OsoCloud::Helpers.params_to_facts(context_facts)
167
+ ))
168
+ result.results
169
+ end
170
+
171
+ ##
172
+ # Add a fact
173
+ #
174
+ # Adds a fact with the given name and arguments.
175
+ #
176
+ # @param name [String]
177
+ # @param args [*[String, OsoCloud::Value]]
178
+ # @return [nil]
179
+ def tell(name, *args)
180
+ typed_args = args.map { |a| OsoCloud::Helpers.extract_value(a)}
181
+ @api.post_facts(OsoCloud::Core::Fact.new(predicate: name, args: typed_args))
182
+ nil
183
+ end
184
+
185
+ ##
186
+ # Add many facts
187
+ #
188
+ # Adds many facts at once.
189
+ #
190
+ # @param facts [Array<fact>]
191
+ # @return [nil]
192
+ # @see Oso more information about facts
193
+ def bulk_tell(facts)
194
+ @api.post_bulk_load(OsoCloud::Helpers.params_to_facts(facts))
195
+ nil
196
+ end
197
+
198
+ ##
199
+ # Delete fact
200
+ #
201
+ # Deletes a fact. Does not throw an error if the fact is not found.
202
+ #
203
+ # @param name [String]
204
+ # @param args [*[String, OsoCloud::Value]]
205
+ # @return [nil]
206
+ def delete(name, *args)
207
+ typed_args = args.map { |a| OsoCloud::Helpers.extract_value(a) }
208
+ @api.delete_facts(OsoCloud::Core::Fact.new(predicate: name, args: typed_args))
209
+ nil
210
+ end
211
+
212
+ ##
213
+ # Delete many facts
214
+ #
215
+ # Deletes many facts at once. Does not throw an error when some of
216
+ # the facts are not found.
217
+ #
218
+ # @param facts [Array<fact>]
219
+ # @return [nil]
220
+ # @see Oso more information about facts
221
+ def bulk_delete(facts)
222
+ @api.post_bulk_delete(OsoCloud::Helpers.params_to_facts(facts))
223
+ nil
224
+ end
225
+
226
+ ##
227
+ # List facts
228
+ #
229
+ # Lists facts that are stored in Oso Cloud. Can be used to check the existence
230
+ # of a particular fact, or used to fetch all facts that have a particular
231
+ # argument. nil arguments operate as wildcards.
232
+ #
233
+ # @param name [String]
234
+ # @param args [*[String, OsoCloud::Value, nil]]
235
+ # @return [Array<fact>]
236
+ # @see Oso more information about facts
237
+ def get(name, *args)
238
+ @api.get_facts(name, args).map do |f|
239
+ name = f.predicate
240
+ args = f.args.map do |a|
241
+ v = OsoCloud::Helpers.from_value(a)
242
+ if v.is_a? Hash
243
+ OsoCloud::Value.new(type: v[:type], id: v[:id])
244
+ else
245
+ v
246
+ end
247
+ end
248
+ [name, *args]
249
+ end
250
+ end
251
+
252
+
253
+ # TODO query, bulk
254
+ end
255
+ end
data/lib/oso/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Oso
2
- VERSION = '0.7.0'.freeze
1
+ module OsoCloud
2
+ VERSION = '1.0.1'.freeze
3
3
  end
data/lib/oso-cloud.rb ADDED
@@ -0,0 +1 @@
1
+ require 'oso/oso'
data/oso-cloud.gemspec CHANGED
@@ -2,13 +2,14 @@ require_relative 'lib/oso/version'
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'oso-cloud'
5
- spec.version = Oso::VERSION
5
+ spec.version = OsoCloud::VERSION
6
6
  spec.authors = ['Oso Security, Inc.']
7
7
  spec.email = ['support@osohq.com']
8
- spec.summary = 'Oso authorization library.'
8
+ spec.summary = 'Oso Cloud Ruby client'
9
9
  spec.homepage = 'https://www.osohq.com/'
10
+ spec.license = 'Apache-2.0'
10
11
 
11
- spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
12
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
12
13
 
13
14
  # Specify which files should be added to the gem when it is released.
14
15
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -19,5 +20,7 @@ Gem::Specification.new do |spec|
19
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
21
  spec.require_paths = ['lib']
21
22
 
23
+ spec.add_dependency 'faraday', '~> 2.5.2'
24
+ spec.add_dependency 'faraday-retry', '~> 2.0.0'
22
25
  spec.add_development_dependency 'minitest', '~> 5.15'
23
26
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oso-cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oso Security, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-24 00:00:00.000000000 Z
11
+ date: 2023-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.5.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.5.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: minitest
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,11 +66,15 @@ files:
38
66
  - Rakefile
39
67
  - bin/console
40
68
  - bin/setup
41
- - lib/oso/client.rb
69
+ - lib/oso-cloud.rb
70
+ - lib/oso/api.rb
71
+ - lib/oso/helpers.rb
72
+ - lib/oso/oso.rb
42
73
  - lib/oso/version.rb
43
74
  - oso-cloud.gemspec
44
75
  homepage: https://www.osohq.com/
45
- licenses: []
76
+ licenses:
77
+ - Apache-2.0
46
78
  metadata: {}
47
79
  post_install_message:
48
80
  rdoc_options: []
@@ -52,15 +84,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
52
84
  requirements:
53
85
  - - ">="
54
86
  - !ruby/object:Gem::Version
55
- version: 2.7.0
87
+ version: 3.0.0
56
88
  required_rubygems_version: !ruby/object:Gem::Requirement
57
89
  requirements:
58
90
  - - ">="
59
91
  - !ruby/object:Gem::Version
60
92
  version: '0'
61
93
  requirements: []
62
- rubygems_version: 3.1.6
94
+ rubygems_version: 3.2.33
63
95
  signing_key:
64
96
  specification_version: 4
65
- summary: Oso authorization library.
97
+ summary: Oso Cloud Ruby client
66
98
  test_files: []
data/lib/oso/client.rb DELETED
@@ -1,198 +0,0 @@
1
- require 'json'
2
- require 'net/http'
3
- require 'uri'
4
-
5
- require 'oso/version'
6
-
7
- module Oso
8
- class Client
9
- def initialize(url: 'https://cloud.osohq.com', api_key: nil)
10
- @url = url
11
- @api_key = api_key
12
- end
13
-
14
- def policy(policy)
15
- POST('policy', { src: policy })
16
- end
17
-
18
- def authorize(actor, action, resource, context_facts = [])
19
- actor_typed_id = extract_typed_id actor
20
- resource_typed_id = extract_typed_id resource
21
- result = POST('authorize', {
22
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
23
- action: action,
24
- resource_type: resource_typed_id.type, resource_id: resource_typed_id.id,
25
- context_facts: facts_to_params(context_facts)
26
- })
27
- allowed = result['allowed']
28
- allowed
29
- end
30
-
31
- def authorize_resources(actor, action, resources, context_facts = [])
32
- return [] if resources.nil?
33
- return [] if resources.empty?
34
-
35
- key = lambda do |type, id|
36
- "#{type}:#{id}"
37
- end
38
-
39
- resources_extracted = resources.map { |r| extract_typed_id(r) }
40
- actor_typed_id = extract_typed_id actor
41
- result = POST('authorize_resources', {
42
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
43
- action: action,
44
- resources: resources_extracted,
45
- context_facts: facts_to_params(context_facts)
46
- })
47
-
48
- return [] if result['results'].empty?
49
-
50
- results_lookup = Hash.new
51
- result['results'].each do |r|
52
- k = key.call(r['type'], r['id'])
53
- if results_lookup[k] == nil
54
- results_lookup[k] = true
55
- end
56
- end
57
-
58
- results = resources.select do |r|
59
- e = extract_typed_id(r)
60
- exists = results_lookup[key.call(e.type, e.id)]
61
- exists
62
- end
63
- results
64
- end
65
-
66
- def list(actor, action, resource_type, context_facts = [])
67
- actor_typed_id = extract_typed_id actor
68
- result = POST('list', {
69
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
70
- action: action,
71
- resource_type: resource_type,
72
- context_facts: facts_to_params(context_facts)
73
- })
74
- results = result['results']
75
- results
76
- end
77
-
78
- def actions(actor, resource, context_facts = [])
79
- actor_typed_id = extract_typed_id actor
80
- resource_typed_id = extract_typed_id resource
81
- result = POST('actions', {
82
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
83
- resource_type: resource_typed_id.type, resource_id: resource_typed_id.id,
84
- context_facts: facts_to_params(context_facts)
85
- })
86
- results = result['results']
87
- results
88
- end
89
-
90
- def tell(predicate, *args)
91
- typed_args = args.map { |a| extract_typed_id a}
92
- POST('facts', { predicate: predicate, args: typed_args })
93
- end
94
-
95
- def bulk_tell(facts)
96
- POST('bulk_load', facts_to_params(facts))
97
- end
98
-
99
- def delete(predicate, *args)
100
- typed_args = args.map { |a| extract_typed_id a}
101
- DELETE('facts', { predicate: predicate, args: typed_args })
102
- end
103
-
104
- def bulk_delete(facts)
105
- POST('bulk_delete', facts_to_params(facts))
106
- end
107
-
108
- def get(predicate, *args)
109
- params = {predicate: predicate}
110
- args.each_with_index do |arg, i|
111
- typed_id = extract_arg_query(arg)
112
- if typed_id
113
- params["args.#{i}.type"] = typed_id.type
114
- params["args.#{i}.id"] = typed_id.id
115
- end
116
- end
117
-
118
- GET('facts', params)
119
- end
120
-
121
- private
122
-
123
- def headers()
124
- {
125
- "Authorization" => "Basic %s" % @api_key,
126
- "User-Agent" => "Oso Cloud (ruby)",
127
- "Accept": "application/json",
128
- "Content-Type": "application/json"
129
- }
130
- end
131
-
132
-
133
- def GET(path, params)
134
- uri = URI("#{@url}/api/#{path}")
135
- uri.query = URI::encode_www_form(params)
136
- use_ssl = (uri.scheme == 'https')
137
-
138
- result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl ) { |http|
139
- http.request(Net::HTTP::Get.new(uri, headers)) {|r|
140
- r.read_body
141
- }
142
- }
143
- handle_result result
144
-
145
- end
146
-
147
- def POST(path, params)
148
- result = Net::HTTP.post(URI("#{@url}/api/#{path}"), params.to_json, headers)
149
- handle_result result
150
- end
151
-
152
- def DELETE(path, params)
153
- uri = URI("#{@url}/api/#{path}")
154
- use_ssl = (uri.scheme == 'https')
155
- result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl ) { |http|
156
- http.request(Net::HTTP::Delete.new(uri, headers), params.to_json) {|r|
157
- r.read_body
158
- }
159
- }
160
- handle_result result
161
- end
162
-
163
- def handle_result(result)
164
- unless result.is_a?(Net::HTTPSuccess)
165
- raise "Got an unexpected error from Oso Service: #{result.code}\n#{result.body}"
166
- end
167
-
168
- JSON.parse(result.body)
169
- end
170
-
171
- def extract_typed_id(x)
172
- return TypedId.new(type: "String", id: x) if x.is_a? String
173
-
174
- raise "#{x} does not have an 'id' field" unless x.respond_to? :id
175
- raise "Invalid 'id' field on #{x}: #{x.id}" if x.id.nil?
176
-
177
- TypedId.new(type: x.class.name, id: x.id.to_s)
178
- end
179
-
180
- def extract_arg_query(x)
181
- return nil if x.nil?
182
- extract_typed_id(x)
183
- end
184
-
185
- def facts_to_params(facts)
186
- facts.map { |predicate, *args|
187
- typed_args = args.map { |a| extract_typed_id a}
188
- { predicate: predicate, args: typed_args }
189
- }
190
- end
191
-
192
- TypedId = Struct.new(:type, :id, keyword_init: true) do
193
- def to_json(*args)
194
- to_h.to_json(*args)
195
- end
196
- end
197
- end
198
- end