oso-cloud 0.7.0 → 1.0.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
  SHA256:
3
- metadata.gz: 69149698f1269f5bd98bf3072671992b364170e0ff5264132fde524f10d997b2
4
- data.tar.gz: ccd52aa2087dfadffc11dce6343aaad7abf10ec3cb2099a8b01df8f19f3e88d4
3
+ metadata.gz: 460a79201e11b1f8e81db2eedea8bfd1c6651301336e1d31652a54a1ed537344
4
+ data.tar.gz: bd3692528348f8e9e45a36c09a3df41cef74fc47258db81471b456e0f1297f32
5
5
  SHA512:
6
- metadata.gz: 2cc987575b2ef6ff3b7bdff48b575c23f1a6739ba5553a44f00731700e03b89a854d71bac3fd60b41bf76854d9b38fb9b380ee2eb7b4abdc7a0338abcb48b3ed
7
- data.tar.gz: afa3922a254311adf4616372c15b3c51c834339d89c5cc3942c86d261e12121d919b8879dd9494e28cb71b7f318f060924e1852cae28e36e7fcf385c70591cb1
6
+ metadata.gz: 360042f1cb0f076651160473ae092215f36773832c70aa042e26a58a2ebbd9aacf034ec7e0227136075fbe1bc000148c627629a6ddee3ca0ba91cc422b1f2818
7
+ data.tar.gz: 79eeab9786a8358d12c3010648ab03f3fff454e69ae217e15122fbc5ebd5e2f4619111da3727e2bb60af14bd3f11e66989c8c74594243852ab131de0e8d1396d
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.0)
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.0)
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,416 @@
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
22
+ attr_reader :message
23
+
24
+ def initialize(message:)
25
+ @message = message
26
+ end
27
+ end
28
+
29
+ # @!visibility private
30
+ class Policy
31
+ attr_reader :filename
32
+ attr_reader :src
33
+
34
+ def initialize(filename:, src:)
35
+ @filename = filename
36
+ @src = src
37
+ end
38
+ end
39
+
40
+ # @!visibility private
41
+ class GetPolicyResult
42
+ attr_reader :policy
43
+
44
+ def initialize(policy:)
45
+ if policy.is_a? Policy
46
+ @policy = policy
47
+ else
48
+ @policy = Policy.new(**policy)
49
+ end
50
+ end
51
+ end
52
+
53
+ # @!visibility private
54
+ class Fact
55
+ attr_reader :predicate
56
+ attr_reader :args
57
+
58
+ def initialize(predicate:, args:)
59
+ @predicate = predicate
60
+ @args = args.map { |v| if v.is_a? Value then v else Value.new(**v) end }
61
+ end
62
+ end
63
+
64
+ # @!visibility private
65
+ class Value
66
+ attr_reader :type
67
+ attr_reader :id
68
+
69
+ def initialize(type:, id:)
70
+ @type = type
71
+ @id = id
72
+ end
73
+ end
74
+
75
+ # @!visibility private
76
+ class Bulk
77
+ attr_reader :delete
78
+ attr_reader :tell
79
+
80
+ def initialize(delete:, tell:)
81
+ @delete = delete.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
82
+ @tell = tell.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
83
+ end
84
+ end
85
+
86
+ # @!visibility private
87
+ class AuthorizeResult
88
+ attr_reader :allowed
89
+
90
+ def initialize(allowed:)
91
+ @allowed = allowed
92
+ end
93
+ end
94
+
95
+ # @!visibility private
96
+ class AuthorizeQuery
97
+ attr_reader :actor_type
98
+ attr_reader :actor_id
99
+ attr_reader :action
100
+ attr_reader :resource_type
101
+ attr_reader :resource_id
102
+ attr_reader :context_facts
103
+
104
+ def initialize(actor_type:, actor_id:, action:, resource_type:, resource_id:, context_facts:)
105
+ @actor_type = actor_type
106
+ @actor_id = actor_id
107
+ @action = action
108
+ @resource_type = resource_type
109
+ @resource_id = resource_id
110
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
111
+ end
112
+ end
113
+
114
+ # @!visibility private
115
+ class AuthorizeResourcesResult
116
+ attr_reader :results
117
+
118
+ def initialize(results:)
119
+ @results = results.map { |v| if v.is_a? Value then v else Value.new(**v) end }
120
+ end
121
+ end
122
+
123
+ # @!visibility private
124
+ class AuthorizeResourcesQuery
125
+ attr_reader :actor_type
126
+ attr_reader :actor_id
127
+ attr_reader :action
128
+ attr_reader :resources
129
+ attr_reader :context_facts
130
+
131
+ def initialize(actor_type:, actor_id:, action:, resources:, context_facts:)
132
+ @actor_type = actor_type
133
+ @actor_id = actor_id
134
+ @action = action
135
+ @resources = resources.map { |v| if v.is_a? Value then v else Value.new(**v) end }
136
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
137
+ end
138
+ end
139
+
140
+ # @!visibility private
141
+ class ListResult
142
+ attr_reader :results
143
+
144
+ def initialize(results:)
145
+ @results = results
146
+ end
147
+ end
148
+
149
+ # @!visibility private
150
+ class ListQuery
151
+ attr_reader :actor_type
152
+ attr_reader :actor_id
153
+ attr_reader :action
154
+ attr_reader :resource_type
155
+ attr_reader :context_facts
156
+
157
+ def initialize(actor_type:, actor_id:, action:, resource_type:, context_facts:)
158
+ @actor_type = actor_type
159
+ @actor_id = actor_id
160
+ @action = action
161
+ @resource_type = resource_type
162
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
163
+ end
164
+ end
165
+
166
+ # @!visibility private
167
+ class ActionsResult
168
+ attr_reader :results
169
+
170
+ def initialize(results:)
171
+ @results = results
172
+ end
173
+ end
174
+
175
+ # @!visibility private
176
+ class ActionsQuery
177
+ attr_reader :actor_type
178
+ attr_reader :actor_id
179
+ attr_reader :resource_type
180
+ attr_reader :resource_id
181
+ attr_reader :context_facts
182
+
183
+ def initialize(actor_type:, actor_id:, resource_type:, resource_id:, context_facts:)
184
+ @actor_type = actor_type
185
+ @actor_id = actor_id
186
+ @resource_type = resource_type
187
+ @resource_id = resource_id
188
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
189
+ end
190
+ end
191
+
192
+ # @!visibility private
193
+ class QueryResult
194
+ attr_reader :results
195
+
196
+ def initialize(results:)
197
+ @results = results.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
198
+ end
199
+ end
200
+
201
+ # @!visibility private
202
+ class Query
203
+ attr_reader :fact
204
+ attr_reader :context_facts
205
+
206
+ def initialize(fact:, context_facts:)
207
+ if fact.is_a? Fact
208
+ @fact = fact
209
+ else
210
+ @fact = Fact.new(**fact)
211
+ end
212
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
213
+ end
214
+ end
215
+
216
+ # @!visibility private
217
+ class StatsResult
218
+ attr_reader :num_roles
219
+ attr_reader :num_relations
220
+ attr_reader :num_facts
221
+
222
+ def initialize(num_roles:, num_relations:, num_facts:)
223
+ @num_roles = num_roles
224
+ @num_relations = num_relations
225
+ @num_facts = num_facts
226
+ end
227
+ end
228
+
229
+
230
+ # @!visibility private
231
+ class Api
232
+ def initialize(url: 'https://cloud.osohq.com', api_key: nil)
233
+ @url = url
234
+ @connection = Faraday.new(url: url) do |faraday|
235
+ faraday.request :retry, {
236
+ max: 15,
237
+ interval: 0.05,
238
+ interval_randomness: 0.5,
239
+ backoff_factor: 2,
240
+ retry_statuses: [429, 500, 502, 503, 504],
241
+ }
242
+
243
+ faraday.request :json
244
+ faraday.response :json, preserve_raw: true
245
+ faraday.adapter :net_http
246
+ end
247
+ @api_key = api_key
248
+ end
249
+
250
+ def get_policy()
251
+ params = {}
252
+ data = nil
253
+ url = "/policy"
254
+ result = GET(url, params, data)
255
+ GetPolicyResult.new(**result)
256
+ end
257
+
258
+ def post_policy(data)
259
+ params = {}
260
+ data = OsoCloud::Helpers.to_hash(data)
261
+ url = "/policy"
262
+ result = POST(url, params, data)
263
+ ApiResult.new(**result)
264
+ end
265
+
266
+ def post_facts(data)
267
+ params = {}
268
+ data = OsoCloud::Helpers.to_hash(data)
269
+ url = "/facts"
270
+ result = POST(url, params, data)
271
+ Fact.new(**result)
272
+ end
273
+
274
+ def delete_facts(data)
275
+ params = {}
276
+ data = OsoCloud::Helpers.to_hash(data)
277
+ url = "/facts"
278
+ result = DELETE(url, params, data)
279
+ ApiResult.new(**result)
280
+ end
281
+
282
+ def post_bulk_load(data)
283
+ params = {}
284
+ data = OsoCloud::Helpers.to_hash(data)
285
+ url = "/bulk_load"
286
+ result = POST(url, params, data)
287
+ ApiResult.new(**result)
288
+ end
289
+
290
+ def post_bulk_delete(data)
291
+ params = {}
292
+ data = OsoCloud::Helpers.to_hash(data)
293
+ url = "/bulk_delete"
294
+ result = POST(url, params, data)
295
+ ApiResult.new(**result)
296
+ end
297
+
298
+ def post_bulk(data)
299
+ params = {}
300
+ data = OsoCloud::Helpers.to_hash(data)
301
+ url = "/bulk"
302
+ result = POST(url, params, data)
303
+ ApiResult.new(**result)
304
+ end
305
+
306
+ def post_authorize(data)
307
+ params = {}
308
+ data = OsoCloud::Helpers.to_hash(data)
309
+ url = "/authorize"
310
+ result = POST(url, params, data)
311
+ AuthorizeResult.new(**result)
312
+ end
313
+
314
+ def post_authorize_resources(data)
315
+ params = {}
316
+ data = OsoCloud::Helpers.to_hash(data)
317
+ url = "/authorize_resources"
318
+ result = POST(url, params, data)
319
+ AuthorizeResourcesResult.new(**result)
320
+ end
321
+
322
+ def post_list(data)
323
+ params = {}
324
+ data = OsoCloud::Helpers.to_hash(data)
325
+ url = "/list"
326
+ result = POST(url, params, data)
327
+ ListResult.new(**result)
328
+ end
329
+
330
+ def post_actions(data)
331
+ params = {}
332
+ data = OsoCloud::Helpers.to_hash(data)
333
+ url = "/actions"
334
+ result = POST(url, params, data)
335
+ ActionsResult.new(**result)
336
+ end
337
+
338
+ def post_query(data)
339
+ params = {}
340
+ data = OsoCloud::Helpers.to_hash(data)
341
+ url = "/query"
342
+ result = POST(url, params, data)
343
+ QueryResult.new(**result)
344
+ end
345
+
346
+ def get_stats()
347
+ params = {}
348
+ data = nil
349
+ url = "/stats"
350
+ result = GET(url, params, data)
351
+ StatsResult.new(**result)
352
+ end
353
+
354
+ def clear_data()
355
+ params = {}
356
+ data = nil
357
+ url = "/clear_data"
358
+ result = POST(url, params, data)
359
+ ApiResult.new(**result)
360
+ end
361
+
362
+
363
+ # hard-coded, not generated
364
+ def get_facts(predicate, args)
365
+ params = {}
366
+ params["predicate"] = predicate
367
+ args.each_with_index do |arg, i|
368
+ arg_query = OsoCloud::Helpers.extract_arg_query(arg)
369
+ if arg_query
370
+ params["args.#{i}.type"] = arg_query.type
371
+ params["args.#{i}.id"] = arg_query.id
372
+ end
373
+ end
374
+ data = nil
375
+ url = "/facts"
376
+ result = GET(url, params, data)
377
+ result.map { |v| Fact.new(**v) }
378
+ end
379
+
380
+ def headers()
381
+ {
382
+ "Authorization" => "Bearer %s" % @api_key,
383
+ "User-Agent" => "Oso Cloud (ruby)",
384
+ "Accept": "application/json",
385
+ "Content-Type": "application/json",
386
+ "X-OsoApiVersion": "0"
387
+ }
388
+ end
389
+
390
+ def GET(path, params, body)
391
+ response = @connection.get("api#{path}", params, headers )
392
+ handle_faraday_response response
393
+ end
394
+
395
+ def POST(path, params, body)
396
+ response = @connection.post("api#{path}", body, headers) do |req|
397
+ req.params = params
398
+ end
399
+ handle_faraday_response response
400
+ end
401
+
402
+ def DELETE(path, params, body)
403
+ response = @connection.delete("api#{path}", params, headers) do |req|
404
+ req.body = body
405
+ end
406
+ handle_faraday_response response
407
+ end
408
+
409
+ def handle_faraday_response(response)
410
+ # TODO:(@patrickod) refactor duplicative JSON parsing
411
+ JSON.parse(response.env[:raw_body], symbolize_names: true)
412
+ end
413
+ end
414
+
415
+ end
416
+ 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.0'.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.0
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: 2022-09-27 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