alula-ruby 1.7.0 → 1.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|