alula-ruby 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION.md +2 -0
- data/lib/alula/api_operations/save.rb +5 -0
- data/lib/alula/client.rb +33 -18
- data/lib/alula/client_configuration.rb +21 -6
- data/lib/alula/errors.rb +4 -0
- data/lib/alula/helpers/jwt_helper.rb +61 -0
- data/lib/alula/resources/token_exchange.rb +60 -12
- data/lib/alula/resources/video/base_resource.rb +1 -1
- data/lib/alula/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a508fa6a1f2b9e59136e9e188458c4ed4e21cb82242dddf5f05523876a391c8d
|
4
|
+
data.tar.gz: d71c8ac244ce5c84d3078aa456f70358a1b8824afd27794b6347b46ff43086e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d281008db754b5774489cb19e00a8a076403ee7319a15d0a4e16991416c570b115fd0ef1b37aacc6d5b16fc65c8f7636670884245385150411d6b1551574eb58
|
7
|
+
data.tar.gz: e04bf33ee07bdc96869782224b336152caeef7ad5169823ef947e6d5d27bec5482698f3249a13cf4038972b8c9d312a3df4c9f249ebd3f8e442909ea60980c75
|
data/VERSION.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
| Version | Date | Description |
|
4
4
|
| ------- | --------- | --------------------------------------------------------------------------- |
|
5
|
+
| v1.8.1 | 2024-06-25 | Update client to accept impersonator authorization to change admin user langauge preference from dealer app |
|
6
|
+
| v1.8.0 | 2024-06-19 | Use Video API endpoints instead of core API passthrough |
|
5
7
|
| v1.7.0 | 2024-06-13 | Add DELETE, PATCH/POST, unregister capabilities for cameras |
|
6
8
|
| v1.6.0 | 2024-01-26 | Add user language attribute |
|
7
9
|
| v1.5.0 | 2024-01-09 | Add more known errors that use a different format |
|
@@ -13,6 +13,7 @@ module Alula
|
|
13
13
|
attributes: as_patchable_json
|
14
14
|
}
|
15
15
|
}
|
16
|
+
payload[:data].delete(:id) if video_request?(resource_url)
|
16
17
|
|
17
18
|
response = Alula::Client.request(:patch, resource_url, payload, {})
|
18
19
|
|
@@ -79,6 +80,10 @@ module Alula
|
|
79
80
|
raise Alula::UnknownError, "Unknown HTTP response code, aborting. Code: #{response.http_status}"
|
80
81
|
end
|
81
82
|
end
|
83
|
+
|
84
|
+
def video_request?(resource_path)
|
85
|
+
resource_path.match(%r{^/video})
|
86
|
+
end
|
82
87
|
end
|
83
88
|
end
|
84
89
|
end
|
data/lib/alula/client.rb
CHANGED
@@ -7,7 +7,7 @@ module Alula
|
|
7
7
|
class << self
|
8
8
|
DEFAULT_CUSTOM_OPTIONS = {
|
9
9
|
omitRelationships: true
|
10
|
-
}
|
10
|
+
}.freeze
|
11
11
|
|
12
12
|
def config
|
13
13
|
@_config ||= Alula::ClientConfiguration.new
|
@@ -18,13 +18,13 @@ module Alula
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def request(http_method, resource_path, filters = {}, opts = {})
|
21
|
-
unless resource_path.match(%r{^/public/v1})
|
22
|
-
validate_request!(http_method, resource_path)
|
23
|
-
end
|
24
|
-
|
21
|
+
validate_request!(http_method, resource_path) unless resource_path.match(%r{^/public/v1})
|
25
22
|
request_opts = build_options(http_method, resource_path, filters, opts)
|
23
|
+
api_url = video_request?(resource_path) ? config.video_api_url : config.api_url
|
24
|
+
request_resource_path = video_request?(resource_path) ? resource_path.gsub(%r{^/video}, '') : resource_path
|
25
|
+
|
26
26
|
# TODO: Handle network failures
|
27
|
-
response = make_request(http_method,
|
27
|
+
response = make_request(http_method, api_url + request_resource_path, request_opts)
|
28
28
|
|
29
29
|
begin
|
30
30
|
resp = AlulaResponse.from_httparty_response(response)
|
@@ -41,7 +41,11 @@ module Alula
|
|
41
41
|
def validate_request!(http_method, resource_path)
|
42
42
|
config.ensure_api_key_set
|
43
43
|
config.ensure_api_url_set
|
44
|
-
|
44
|
+
if video_request?(resource_path)
|
45
|
+
config.ensure_video_api_url_set
|
46
|
+
config.ensure_video_api_key_set
|
47
|
+
config.ensure_customer_id_set
|
48
|
+
end
|
45
49
|
ensure_method_allowable(http_method)
|
46
50
|
config.ensure_role_set if %i[patch post put].include? http_method
|
47
51
|
end
|
@@ -51,18 +55,24 @@ module Alula
|
|
51
55
|
end
|
52
56
|
|
53
57
|
def ensure_method_allowable(http_method)
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
return if %i[get post put patch delete].include?(http_method)
|
59
|
+
|
60
|
+
raise "Unable to send a request with #{http_method} http method"
|
61
|
+
end
|
62
|
+
|
63
|
+
def video_request?(resource_path)
|
64
|
+
resource_path.match(%r{^/video})
|
57
65
|
end
|
58
66
|
|
59
67
|
def build_options(http_method, resource_path, filters = {}, opts = {})
|
68
|
+
api_key = Alula::Client.config.impersonator_api_key || Alula::Client.config.api_key
|
60
69
|
request_opts = {
|
61
70
|
headers: {
|
62
|
-
'Authorization': "Bearer #{
|
71
|
+
'Authorization': "Bearer #{api_key}",
|
63
72
|
'User-Agent': "#{Alula::Client.config.user_agent || 'No Agent Set'}/alula-ruby v#{Alula::VERSION}"
|
64
73
|
}
|
65
74
|
}.merge(opts)
|
75
|
+
handle_video_request(request_opts) if video_request?(resource_path) # Add token for all video requests
|
66
76
|
|
67
77
|
request_opts[:headers]['Content-Type'] = 'application/json' unless opts[:multipart]
|
68
78
|
case http_method
|
@@ -70,11 +80,7 @@ module Alula
|
|
70
80
|
:post
|
71
81
|
request_opts[:body] = opts[:multipart] ? filters : JSON.generate(filters)
|
72
82
|
when :get
|
73
|
-
if resource_path.match(%r{^/(rest|rpc)})
|
74
|
-
request_opts = request_opts.merge build_rest_options(filters)
|
75
|
-
elsif resource_path.match(%r{^/video})
|
76
|
-
add_customer_id(request_opts)
|
77
|
-
end
|
83
|
+
request_opts = request_opts.merge build_rest_options(filters) if resource_path.match(%r{^/(rest|rpc)})
|
78
84
|
when :delete
|
79
85
|
# Nothing special
|
80
86
|
end
|
@@ -111,8 +117,17 @@ module Alula
|
|
111
117
|
filters
|
112
118
|
end
|
113
119
|
|
114
|
-
def
|
115
|
-
|
120
|
+
def handle_video_request(request_opts)
|
121
|
+
if Alula::Client.config.internal
|
122
|
+
set_token(request_opts, { internal: true })
|
123
|
+
else
|
124
|
+
set_token(request_opts, { customerId: Alula::Client.config.customer_id })
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_token(request_opts, payload)
|
129
|
+
jwt = TokenExchange.fetch_video_token(payload)
|
130
|
+
request_opts[:headers]['token'] = jwt
|
116
131
|
end
|
117
132
|
end
|
118
133
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module Alula
|
2
2
|
class ClientConfiguration
|
3
|
-
attr_accessor :api_url, :debug, :user_agent
|
4
|
-
|
3
|
+
attr_accessor :api_url, :video_api_url, :debug, :user_agent
|
4
|
+
|
5
|
+
REQUEST_ATTRIBUTES = %i[api_key video_api_key customer_id internal impersonator_api_key]
|
5
6
|
|
6
7
|
# Using RequestStore so we're thread safe even with our non-thread-safe architecture :(
|
7
8
|
REQUEST_ATTRIBUTES.each do |prop|
|
@@ -42,11 +43,25 @@ module Alula
|
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
46
|
+
def ensure_video_api_url_set
|
47
|
+
return unless video_api_url.nil?
|
48
|
+
|
49
|
+
raise Alula::NotConfiguredError,
|
50
|
+
'did you forget to set the Alula::Client.config.video_api_url config option?'
|
51
|
+
end
|
52
|
+
|
53
|
+
def ensure_video_api_key_set
|
54
|
+
return if video_api_key
|
55
|
+
|
56
|
+
raise Alula::NotConfiguredError,
|
57
|
+
'Set your video API access token before making requests to the video API'
|
58
|
+
end
|
59
|
+
|
45
60
|
def ensure_customer_id_set
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
61
|
+
return if internal || customer_id
|
62
|
+
|
63
|
+
raise Alula::NotConfiguredError,
|
64
|
+
'Set your customer_id before making requests to the video API'
|
50
65
|
end
|
51
66
|
|
52
67
|
def ensure_role_set
|
data/lib/alula/errors.rb
CHANGED
@@ -87,6 +87,8 @@ module Alula
|
|
87
87
|
NotFoundError.new(message)
|
88
88
|
when 'Device Not Found'
|
89
89
|
NotFoundError.new(message)
|
90
|
+
when 'Unable to unregister device'
|
91
|
+
NotFoundError.new(message)
|
90
92
|
else
|
91
93
|
Alula.logger.error response
|
92
94
|
raise NotImplementedError, "Unable to derive error for #{response.data['error'].to_json}"
|
@@ -128,6 +130,8 @@ module Alula
|
|
128
130
|
NotFoundError.new(message)
|
129
131
|
when 'Device Not Found'
|
130
132
|
NotFoundError.new(message)
|
133
|
+
when 'Unable to unregister device'
|
134
|
+
NotFoundError.new(message)
|
131
135
|
else
|
132
136
|
Alula.logger.error response
|
133
137
|
raise NotImplementedError, "Unable to derive error for #{response.data['errors'].to_json}"
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
module Alula
|
6
|
+
# Helper class to build JWT tokens
|
7
|
+
# Used for internal video API calls or video API calls with a customer ID
|
8
|
+
module JwtHelper
|
9
|
+
ONE_HOUR = 60 * 60
|
10
|
+
class << self
|
11
|
+
def build_jwt_token(jwt_payload)
|
12
|
+
jwt_secret = Alula::Client.config.video_api_key
|
13
|
+
|
14
|
+
header = build_jwt_header
|
15
|
+
|
16
|
+
token_data = add_timestamp_to_payload(jwt_payload)
|
17
|
+
|
18
|
+
encoded_header = convert_to_base64(JSON.generate(header))
|
19
|
+
encoded_data = convert_to_base64(JSON.generate(token_data))
|
20
|
+
|
21
|
+
token = "#{encoded_header}.#{encoded_data}"
|
22
|
+
signature = sign_token(token, jwt_secret)
|
23
|
+
|
24
|
+
"#{token}.#{signature}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_jwt_header
|
28
|
+
{
|
29
|
+
'typ' => 'JWT',
|
30
|
+
'alg' => 'HS256'
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_timestamp_to_payload(payload)
|
35
|
+
current_timestamp = Time.now.to_i
|
36
|
+
payload.merge(
|
37
|
+
{
|
38
|
+
'iat' => current_timestamp,
|
39
|
+
'exp' => current_timestamp + ONE_HOUR # expiry time is 60 minutes from time of creation
|
40
|
+
}
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def sign_token(token, secret)
|
45
|
+
signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret, token)
|
46
|
+
convert_to_base64(signature)
|
47
|
+
end
|
48
|
+
|
49
|
+
def convert_to_base64(source)
|
50
|
+
# Encode in classical base64,
|
51
|
+
encoded_source = Base64.strict_encode64(source)
|
52
|
+
|
53
|
+
# Remove padding equal characters,
|
54
|
+
encoded_source = encoded_source.gsub('=', '')
|
55
|
+
|
56
|
+
# Replace characters according to base64url specifications,
|
57
|
+
encoded_source.tr('+/', '-_')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,17 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../helpers/jwt_helper'
|
4
|
+
|
1
5
|
module Alula
|
6
|
+
# Helper class to generate OAuth access tokens (core API) and JWT tokens (VSP API)
|
2
7
|
class TokenExchange
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
class << self
|
9
|
+
def token_for_user(user_id)
|
10
|
+
url = '/rest/v1/oauth/accesstokens'
|
11
|
+
payload = { data: { attributes: { userId: user_id } } }
|
12
|
+
opts = {}
|
13
|
+
|
14
|
+
response = Alula::Client.request(:post, url, payload, opts)
|
15
|
+
|
16
|
+
if response.ok?
|
17
|
+
ImpersonatedToken.new(response.data['data']['attributes'])
|
18
|
+
else
|
19
|
+
error_class = AlulaError.for_response(response)
|
20
|
+
raise error_class
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_video_token(payload)
|
25
|
+
cache_key = generate_cache_key(payload)
|
26
|
+
cached_token, expiry = retrieve_cached_token(cache_key)
|
27
|
+
|
28
|
+
if cached_token && Time.now.to_i < expiry
|
29
|
+
jwt_token = cached_token
|
30
|
+
else
|
31
|
+
jwt_token = build_jwt_token(payload)
|
32
|
+
expiry = Time.now.to_i + JwtHelper::ONE_HOUR # 1 hour expiry
|
33
|
+
store_token_in_cache(cache_key, jwt_token, expiry)
|
34
|
+
end
|
35
|
+
|
36
|
+
jwt_token
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_jwt_token(payload)
|
42
|
+
JwtHelper.build_jwt_token(payload)
|
43
|
+
end
|
44
|
+
|
45
|
+
def store_token_in_cache(cache_key, jwt_token, expiry)
|
46
|
+
@token_cache ||= {}
|
47
|
+
@token_cache[cache_key] = [jwt_token, expiry]
|
48
|
+
end
|
49
|
+
|
50
|
+
def retrieve_cached_token(cache_key)
|
51
|
+
@token_cache ||= {}
|
52
|
+
@token_cache[cache_key]
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_cache_key(payload)
|
56
|
+
if payload[:internal]
|
57
|
+
'jwt_token_internal'
|
58
|
+
elsif payload[:customerId]
|
59
|
+
"jwt_token_customer_#{payload[:customerId]}"
|
60
|
+
else
|
61
|
+
raise ArgumentError, 'Invalid payload'
|
62
|
+
end
|
15
63
|
end
|
16
64
|
end
|
17
65
|
|
data/lib/alula/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alula-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Titus Johnson
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-06-
|
11
|
+
date: 2024-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: httparty
|
@@ -210,6 +210,7 @@ files:
|
|
210
210
|
- lib/alula/errors.rb
|
211
211
|
- lib/alula/filter_builder.rb
|
212
212
|
- lib/alula/helpers/device_attribute_translations.rb
|
213
|
+
- lib/alula/helpers/jwt_helper.rb
|
213
214
|
- lib/alula/list_object.rb
|
214
215
|
- lib/alula/meta.rb
|
215
216
|
- lib/alula/monkey_patches.rb
|