azure-blob 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.terraform.lock.hcl +22 -0
- data/CHANGELOG.md +15 -2
- data/README.md +94 -22
- data/Rakefile +51 -1
- data/azure-blob.gemspec +3 -2
- data/devenv.lock +11 -11
- data/devenv.nix +27 -3
- data/devenv.yaml +1 -1
- data/input.tf +44 -0
- data/lib/active_storage/service/azure_blob_service.rb +4 -4
- data/lib/azure_blob/blob.rb +7 -0
- data/lib/azure_blob/blob_list.rb +18 -0
- data/lib/azure_blob/block_list.rb +3 -1
- data/lib/azure_blob/canonicalized_headers.rb +1 -1
- data/lib/azure_blob/canonicalized_resource.rb +1 -1
- data/lib/azure_blob/client.rb +126 -5
- data/lib/azure_blob/entra_id_signer.rb +115 -0
- data/lib/azure_blob/http.rb +19 -3
- data/lib/azure_blob/identity_token.rb +65 -0
- data/lib/azure_blob/metadata.rb +1 -1
- data/lib/azure_blob/{signer.rb → shared_key_signer.rb} +6 -6
- data/lib/azure_blob/user_delegation_key.rb +67 -0
- data/lib/azure_blob/version.rb +1 -1
- data/main.tf +187 -0
- data/output.tf +30 -0
- metadata +14 -6
data/lib/azure_blob/client.rb
CHANGED
@@ -1,21 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "signer"
|
4
3
|
require_relative "block_list"
|
5
4
|
require_relative "blob_list"
|
6
5
|
require_relative "blob"
|
7
6
|
require_relative "http"
|
7
|
+
require_relative "shared_key_signer"
|
8
|
+
require_relative "entra_id_signer"
|
8
9
|
require "time"
|
9
10
|
require "base64"
|
10
11
|
|
11
12
|
module AzureBlob
|
13
|
+
# AzureBlob Client class. You interact with the Azure Blob api
|
14
|
+
# through an instance of this class.
|
12
15
|
class Client
|
13
|
-
def initialize(account_name:, access_key:, container
|
16
|
+
def initialize(account_name:, access_key:, container:, **options)
|
14
17
|
@account_name = account_name
|
15
18
|
@container = container
|
16
|
-
|
19
|
+
|
20
|
+
@signer = !access_key.nil? && !access_key.empty? ?
|
21
|
+
AzureBlob::SharedKeySigner.new(account_name:, access_key:) :
|
22
|
+
AzureBlob::EntraIdSigner.new(account_name:, **options.slice(:principal_id))
|
17
23
|
end
|
18
24
|
|
25
|
+
# Create a blob of type block. Will automatically split the the blob in multiple block and send the blob in pieces (blocks) if the blob is too big.
|
26
|
+
#
|
27
|
+
# When the blob is small enough this method will send the blob through {Put Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob]
|
28
|
+
#
|
29
|
+
# If the blob is too big, the blob is split in blocks sent through a series of {Put Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block] requests
|
30
|
+
# followed by a {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list] to commit the block list.
|
31
|
+
#
|
32
|
+
# Takes a key (path), the content (String or IO object), and options.
|
33
|
+
#
|
34
|
+
# Options:
|
35
|
+
#
|
36
|
+
# [+:content_type+]
|
37
|
+
# Will be saved on the blob in Azure.
|
38
|
+
# [+:content_disposition+]
|
39
|
+
# Will be saved on the blob in Azure.
|
40
|
+
# [+:content_md5+]
|
41
|
+
# Will ensure integrity of the upload. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
|
42
|
+
# The checksum is only checked on a single upload! To verify checksum when uploading multiple blocks, call directly put_blob_block with
|
43
|
+
# a checksum for each block, then commit the blocks with commit_blob_blocks.
|
44
|
+
# [+:block_size+]
|
45
|
+
# Block size in bytes, can be used to force the method to split the upload in smaller chunk. Defaults to +AzureBlob::DEFAULT_BLOCK_SIZE+ and cannot be bigger than +AzureBlob::MAX_UPLOAD_SIZE+
|
19
46
|
def create_block_blob(key, content, options = {})
|
20
47
|
if content.size > (options[:block_size] || DEFAULT_BLOCK_SIZE)
|
21
48
|
put_blob_multiple(key, content, **options)
|
@@ -24,6 +51,18 @@ module AzureBlob
|
|
24
51
|
end
|
25
52
|
end
|
26
53
|
|
54
|
+
# Returns the full or partial content of the blob
|
55
|
+
#
|
56
|
+
# Calls to the {Get Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob] endpoint.
|
57
|
+
#
|
58
|
+
# Takes a key (path) and options.
|
59
|
+
#
|
60
|
+
# Options:
|
61
|
+
#
|
62
|
+
# [+:start+]
|
63
|
+
# Starting point in bytes
|
64
|
+
# [+:end+]
|
65
|
+
# Ending point in bytes
|
27
66
|
def get_blob(key, options = {})
|
28
67
|
uri = generate_uri("#{container}/#{key}")
|
29
68
|
|
@@ -34,6 +73,15 @@ module AzureBlob
|
|
34
73
|
Http.new(uri, headers, signer:).get
|
35
74
|
end
|
36
75
|
|
76
|
+
# Delete a blob
|
77
|
+
#
|
78
|
+
# Calls to {Delete Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob]
|
79
|
+
#
|
80
|
+
# Takes a key (path) and options.
|
81
|
+
#
|
82
|
+
# Options:
|
83
|
+
# [+:delete_snapshots+]
|
84
|
+
# Sets the value of the x-ms-delete-snapshots header. Default to +include+
|
37
85
|
def delete_blob(key, options = {})
|
38
86
|
uri = generate_uri("#{container}/#{key}")
|
39
87
|
|
@@ -44,11 +92,28 @@ module AzureBlob
|
|
44
92
|
Http.new(uri, headers, signer:).delete
|
45
93
|
end
|
46
94
|
|
95
|
+
# Delete all blobs prefixed by the given prefix.
|
96
|
+
#
|
97
|
+
# Calls to {List blobs}[https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs]
|
98
|
+
# followed to a series of calls to {Delete Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob]
|
99
|
+
#
|
100
|
+
# Takes a prefix and options
|
101
|
+
#
|
102
|
+
# Look delete_blob for the list of options.
|
47
103
|
def delete_prefix(prefix, options = {})
|
48
104
|
results = list_blobs(prefix:)
|
49
105
|
results.each { |key| delete_blob(key) }
|
50
106
|
end
|
51
107
|
|
108
|
+
# Returns a BlobList containing a list of keys (paths)
|
109
|
+
#
|
110
|
+
# Calls to {List blobs}[https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs]
|
111
|
+
#
|
112
|
+
# Options:
|
113
|
+
# [+:prefix+]
|
114
|
+
# Prefix of the blobs to be listed. Defaults to listing everything in the container.
|
115
|
+
# [:+max_results+]
|
116
|
+
# Maximum number of results to return per page.
|
52
117
|
def list_blobs(options = {})
|
53
118
|
uri = generate_uri(container)
|
54
119
|
query = {
|
@@ -69,6 +134,11 @@ module AzureBlob
|
|
69
134
|
BlobList.new(fetcher)
|
70
135
|
end
|
71
136
|
|
137
|
+
# Returns a Blob object without the content.
|
138
|
+
#
|
139
|
+
# Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]
|
140
|
+
#
|
141
|
+
# This can be used to see if the blob exist or obtain metada such as content type, disposition, checksum or Azure custom metadata.
|
72
142
|
def get_blob_properties(key, options = {})
|
73
143
|
uri = generate_uri("#{container}/#{key}")
|
74
144
|
|
@@ -77,16 +147,37 @@ module AzureBlob
|
|
77
147
|
Blob.new(response)
|
78
148
|
end
|
79
149
|
|
150
|
+
# Return a URI object to a resource in the container. Takes a path.
|
151
|
+
#
|
152
|
+
# Example: +generate_uri("#{container}/#{key}")+
|
80
153
|
def generate_uri(path)
|
81
154
|
URI.parse(URI::DEFAULT_PARSER.escape(File.join(host, path)))
|
82
155
|
end
|
83
156
|
|
157
|
+
# Returns an SAS signed URI
|
158
|
+
#
|
159
|
+
# Takes a
|
160
|
+
# - key (path)
|
161
|
+
# - A permission string (+"r"+, +"rw"+)
|
162
|
+
# - expiry as a UTC iso8601 time string
|
163
|
+
# - options
|
84
164
|
def signed_uri(key, permissions:, expiry:, **options)
|
85
165
|
uri = generate_uri("#{container}/#{key}")
|
86
166
|
uri.query = signer.sas_token(uri, permissions:, expiry:, **options)
|
87
167
|
uri
|
88
168
|
end
|
89
169
|
|
170
|
+
# Creates a Blob of type append.
|
171
|
+
#
|
172
|
+
# Calls to {Put Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob]
|
173
|
+
#
|
174
|
+
# You are expected to append blocks to the blob with append_blob_block after creating the blob.
|
175
|
+
# Options:
|
176
|
+
#
|
177
|
+
# [+:content_type+]
|
178
|
+
# Will be saved on the blob in Azure.
|
179
|
+
# [+:content_disposition+]
|
180
|
+
# Will be saved on the blob in Azure.
|
90
181
|
def create_append_blob(key, options = {})
|
91
182
|
uri = generate_uri("#{container}/#{key}")
|
92
183
|
|
@@ -101,6 +192,15 @@ module AzureBlob
|
|
101
192
|
Http.new(uri, headers, metadata: options[:metadata], signer:).put(nil)
|
102
193
|
end
|
103
194
|
|
195
|
+
# Append a block to an Append Blob
|
196
|
+
#
|
197
|
+
# Calls to {Append Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/append-block]
|
198
|
+
#
|
199
|
+
# Options:
|
200
|
+
#
|
201
|
+
# [+:content_md5+]
|
202
|
+
# Will ensure integrity of the upload. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
|
203
|
+
# The checksum must be the checksum of the block not the blob.
|
104
204
|
def append_blob_block(key, content, options = {})
|
105
205
|
uri = generate_uri("#{container}/#{key}")
|
106
206
|
uri.query = URI.encode_www_form(comp: "appendblock")
|
@@ -114,6 +214,16 @@ module AzureBlob
|
|
114
214
|
Http.new(uri, headers, signer:).put(content)
|
115
215
|
end
|
116
216
|
|
217
|
+
# Uploads a block to a blob.
|
218
|
+
#
|
219
|
+
# Calls to {Put Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block]
|
220
|
+
#
|
221
|
+
# Returns the id of the block. Required to commit the list of blocks to a blob.
|
222
|
+
#
|
223
|
+
# Options:
|
224
|
+
#
|
225
|
+
# [+:content_md5+]
|
226
|
+
# Must be the checksum for the block not the blob. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
|
117
227
|
def put_blob_block(key, index, content, options = {})
|
118
228
|
block_id = generate_block_id(index)
|
119
229
|
uri = generate_uri("#{container}/#{key}")
|
@@ -130,6 +240,17 @@ module AzureBlob
|
|
130
240
|
block_id
|
131
241
|
end
|
132
242
|
|
243
|
+
# Commits the list of blocks to a blob.
|
244
|
+
#
|
245
|
+
# Calls to {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list]
|
246
|
+
#
|
247
|
+
# Takes a key (path) and an array of block ids
|
248
|
+
#
|
249
|
+
# Options:
|
250
|
+
#
|
251
|
+
# [+:content_md5+]
|
252
|
+
# This is the checksum for the whole blob. The checksum is saved on the blob, but it is not validated!
|
253
|
+
# Add a checksum for each block if you want Azure to validate integrity.
|
133
254
|
def commit_blob_blocks(key, block_ids, options = {})
|
134
255
|
block_list = BlockList.new(block_ids)
|
135
256
|
content = block_list.to_s
|
@@ -139,7 +260,7 @@ module AzureBlob
|
|
139
260
|
headers = {
|
140
261
|
"Content-Length": content.size,
|
141
262
|
"Content-Type": options[:content_type],
|
142
|
-
"
|
263
|
+
"x-ms-blob-content-md5": options[:content_md5],
|
143
264
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
144
265
|
}
|
145
266
|
|
@@ -171,7 +292,7 @@ module AzureBlob
|
|
171
292
|
"x-ms-blob-type": "BlockBlob",
|
172
293
|
"Content-Length": content.size,
|
173
294
|
"Content-Type": options[:content_type],
|
174
|
-
"
|
295
|
+
"x-ms-blob-content-md5": options[:content_md5],
|
175
296
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
176
297
|
}
|
177
298
|
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "openssl"
|
3
|
+
require "net/http"
|
4
|
+
require "rexml/document"
|
5
|
+
|
6
|
+
require_relative "canonicalized_resource"
|
7
|
+
require_relative "identity_token"
|
8
|
+
|
9
|
+
require_relative "user_delegation_key"
|
10
|
+
|
11
|
+
module AzureBlob
|
12
|
+
class EntraIdSigner # :nodoc:
|
13
|
+
attr_reader :token
|
14
|
+
attr_reader :account_name
|
15
|
+
|
16
|
+
def initialize(account_name:, principal_id: nil)
|
17
|
+
@token = AzureBlob::IdentityToken.new(principal_id:)
|
18
|
+
@account_name = account_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorization_header(uri:, verb:, headers: {})
|
22
|
+
"Bearer #{token}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def sas_token(uri, options = {})
|
26
|
+
to_sign = [
|
27
|
+
options[:permissions],
|
28
|
+
options[:start],
|
29
|
+
options[:expiry],
|
30
|
+
CanonicalizedResource.new(uri, account_name, url_safe: false, service_name: :blob),
|
31
|
+
delegation_key.signed_oid,
|
32
|
+
delegation_key.signed_tid,
|
33
|
+
delegation_key.signed_start,
|
34
|
+
delegation_key.signed_expiry,
|
35
|
+
delegation_key.signed_service,
|
36
|
+
delegation_key.signed_version,
|
37
|
+
nil,
|
38
|
+
nil,
|
39
|
+
nil,
|
40
|
+
options[:ip],
|
41
|
+
options[:protocol],
|
42
|
+
SAS::Version,
|
43
|
+
SAS::Resources::Blob,
|
44
|
+
nil,
|
45
|
+
nil,
|
46
|
+
nil,
|
47
|
+
options[:content_disposition],
|
48
|
+
nil,
|
49
|
+
nil,
|
50
|
+
options[:content_type],
|
51
|
+
].join("\n")
|
52
|
+
|
53
|
+
query = {
|
54
|
+
SAS::Fields::Permissions => options[:permissions],
|
55
|
+
SAS::Fields::Start => options[:start],
|
56
|
+
SAS::Fields::Expiry => options[:expiry],
|
57
|
+
|
58
|
+
SAS::Fields::SignedObjectId => delegation_key.signed_oid,
|
59
|
+
SAS::Fields::SignedTenantId => delegation_key.signed_tid,
|
60
|
+
SAS::Fields::SignedKeyStartTime => delegation_key.signed_start,
|
61
|
+
SAS::Fields::SignedKeyExpiryTime => delegation_key.signed_expiry,
|
62
|
+
SAS::Fields::SignedKeyService => delegation_key.signed_service,
|
63
|
+
SAS::Fields::Signedkeyversion => delegation_key.signed_version,
|
64
|
+
|
65
|
+
|
66
|
+
SAS::Fields::SignedIp => options[:ip],
|
67
|
+
SAS::Fields::SignedProtocol => options[:protocol],
|
68
|
+
SAS::Fields::Version => SAS::Version,
|
69
|
+
SAS::Fields::Resource => SAS::Resources::Blob,
|
70
|
+
|
71
|
+
SAS::Fields::Disposition => options[:content_disposition],
|
72
|
+
SAS::Fields::Type => options[:content_type],
|
73
|
+
SAS::Fields::Signature => sign(to_sign, key: delegation_key.to_s),
|
74
|
+
|
75
|
+
}.reject { |_, value| value.nil? }
|
76
|
+
|
77
|
+
URI.encode_www_form(**query)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def delegation_key
|
83
|
+
@delegation_key ||= UserDelegationKey.new(account_name:, signer: self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def sign(body, key:)
|
87
|
+
Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", key, body))
|
88
|
+
end
|
89
|
+
|
90
|
+
module SAS # :nodoc:
|
91
|
+
Version = "2024-05-04"
|
92
|
+
module Fields # :nodoc:
|
93
|
+
Permissions = :sp
|
94
|
+
Version = :sv
|
95
|
+
Start = :st
|
96
|
+
Expiry = :se
|
97
|
+
Resource = :sr
|
98
|
+
Signature = :sig
|
99
|
+
Disposition = :rscd
|
100
|
+
Type = :rsct
|
101
|
+
SignedObjectId = :skoid
|
102
|
+
SignedTenantId = :sktid
|
103
|
+
SignedKeyStartTime = :skt
|
104
|
+
SignedKeyExpiryTime = :ske
|
105
|
+
SignedKeyService = :sks
|
106
|
+
Signedkeyversion = :skv
|
107
|
+
SignedIp = :sip
|
108
|
+
SignedProtocol = :spr
|
109
|
+
end
|
110
|
+
module Resources # :nodoc:
|
111
|
+
Blob = :b
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/azure_blob/http.rb
CHANGED
@@ -6,8 +6,15 @@ require "net/http"
|
|
6
6
|
require "rexml"
|
7
7
|
|
8
8
|
module AzureBlob
|
9
|
-
class Http
|
10
|
-
class Error < AzureBlob::Error
|
9
|
+
class Http # :nodoc:
|
10
|
+
class Error < AzureBlob::Error
|
11
|
+
attr_reader :body, :status
|
12
|
+
def initialize(body: nil, status: nil)
|
13
|
+
@body = body
|
14
|
+
@status = status
|
15
|
+
super(body)
|
16
|
+
end
|
17
|
+
end
|
11
18
|
class FileNotFoundError < Error; end
|
12
19
|
class ForbidenError < Error; end
|
13
20
|
class IntegrityError < Error; end
|
@@ -44,6 +51,15 @@ module AzureBlob
|
|
44
51
|
true
|
45
52
|
end
|
46
53
|
|
54
|
+
def post(content)
|
55
|
+
sign_request("POST") if signer
|
56
|
+
@response = http.start do |http|
|
57
|
+
http.post(uri, content, headers)
|
58
|
+
end
|
59
|
+
raise_error unless success?
|
60
|
+
response.body
|
61
|
+
end
|
62
|
+
|
47
63
|
def head
|
48
64
|
sign_request("HEAD") if signer
|
49
65
|
@response = http.start do |http|
|
@@ -91,7 +107,7 @@ module AzureBlob
|
|
91
107
|
end
|
92
108
|
|
93
109
|
def raise_error
|
94
|
-
raise error_from_response.new(@response.body)
|
110
|
+
raise error_from_response.new(body: @response.body, status: @response.code&.to_i)
|
95
111
|
end
|
96
112
|
|
97
113
|
def status
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module AzureBlob
|
4
|
+
class IdentityToken
|
5
|
+
RESOURCE_URI = "https://storage.azure.com/"
|
6
|
+
EXPIRATION_BUFFER = 600 # 10 minutes
|
7
|
+
|
8
|
+
IDENTITY_ENDPOINT = ENV["IDENTITY_ENDPOINT"] || "http://169.254.169.254/metadata/identity/oauth2/token"
|
9
|
+
API_VERSION = ENV["IDENTITY_ENDPOINT"] ? "2019-08-01" : "2018-02-01"
|
10
|
+
|
11
|
+
def initialize(principal_id: nil)
|
12
|
+
@identity_uri = URI.parse(IDENTITY_ENDPOINT)
|
13
|
+
params = {
|
14
|
+
'api-version': API_VERSION,
|
15
|
+
resource: RESOURCE_URI,
|
16
|
+
}
|
17
|
+
params[:principal_id] = principal_id if principal_id
|
18
|
+
@identity_uri.query = URI.encode_www_form(params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
refresh if expired?
|
23
|
+
token
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def expired?
|
29
|
+
token.nil? || Time.now >= (expiration - EXPIRATION_BUFFER)
|
30
|
+
end
|
31
|
+
|
32
|
+
def refresh
|
33
|
+
headers = { "Metadata" => "true" }
|
34
|
+
headers["X-IDENTITY-HEADER"] = ENV["IDENTITY_HEADER"] if ENV["IDENTITY_HEADER"]
|
35
|
+
|
36
|
+
attempt = 0
|
37
|
+
begin
|
38
|
+
attempt += 1
|
39
|
+
response = JSON.parse(AzureBlob::Http.new(identity_uri, headers).get)
|
40
|
+
rescue AzureBlob::Http::Error => error
|
41
|
+
if should_retry?(error, attempt)
|
42
|
+
attempt = 1 if error.status == 410
|
43
|
+
delay = exponential_backoff(error, attempt)
|
44
|
+
Kernel.sleep(delay)
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
@token = response["access_token"]
|
50
|
+
@expiration = Time.at(response["expires_on"].to_i)
|
51
|
+
end
|
52
|
+
|
53
|
+
def should_retry?(error, attempt)
|
54
|
+
is_500 = error.status/500 == 1
|
55
|
+
(is_500 || [ 404, 408, 410, 429 ].include?(error.status)) && attempt < 5
|
56
|
+
end
|
57
|
+
|
58
|
+
def exponential_backoff(error, attempt)
|
59
|
+
EXPONENTIAL_BACKOFF[attempt -1] || raise(AzureBlob::Error.new("Exponential backoff out of bounds!"))
|
60
|
+
end
|
61
|
+
EXPONENTIAL_BACKOFF = [ 2, 6, 14, 30 ]
|
62
|
+
|
63
|
+
attr_reader :identity_uri, :expiration, :token
|
64
|
+
end
|
65
|
+
end
|
data/lib/azure_blob/metadata.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative "canonicalized_headers"
|
|
6
6
|
require_relative "canonicalized_resource"
|
7
7
|
|
8
8
|
module AzureBlob
|
9
|
-
class
|
9
|
+
class SharedKeySigner # :nodoc:
|
10
10
|
def initialize(account_name:, access_key:)
|
11
11
|
@account_name = account_name
|
12
12
|
@access_key = Base64.decode64(access_key)
|
@@ -71,21 +71,21 @@ module AzureBlob
|
|
71
71
|
URI.encode_www_form(**query)
|
72
72
|
end
|
73
73
|
|
74
|
+
private
|
75
|
+
|
74
76
|
def sign(body)
|
75
77
|
Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", access_key, body))
|
76
78
|
end
|
77
79
|
|
78
|
-
private
|
79
|
-
|
80
80
|
def sanitize_headers(headers)
|
81
81
|
headers = headers.dup
|
82
82
|
headers[:"Content-Length"] = nil if headers[:"Content-Length"].to_i == 0
|
83
83
|
headers
|
84
84
|
end
|
85
85
|
|
86
|
-
module SAS
|
86
|
+
module SAS # :nodoc:
|
87
87
|
Version = "2024-05-04"
|
88
|
-
module Fields
|
88
|
+
module Fields # :nodoc:
|
89
89
|
Permissions = :sp
|
90
90
|
Version = :sv
|
91
91
|
Expiry = :se
|
@@ -94,7 +94,7 @@ module AzureBlob
|
|
94
94
|
Disposition = :rscd
|
95
95
|
Type = :rsct
|
96
96
|
end
|
97
|
-
module Resources
|
97
|
+
module Resources # :nodoc:
|
98
98
|
Blob = :b
|
99
99
|
end
|
100
100
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative "http"
|
2
|
+
|
3
|
+
module AzureBlob
|
4
|
+
class UserDelegationKey # :nodoc:
|
5
|
+
EXPIRATION = 25200 # 7 hours
|
6
|
+
EXPIRATION_BUFFER = 3600 # 1 hours
|
7
|
+
def initialize(account_name:, signer:)
|
8
|
+
@uri = URI.parse(
|
9
|
+
"https://#{account_name}.blob.core.windows.net/?restype=service&comp=userdelegationkey"
|
10
|
+
)
|
11
|
+
|
12
|
+
@signer = signer
|
13
|
+
|
14
|
+
refresh
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
user_delegation_key
|
19
|
+
end
|
20
|
+
|
21
|
+
def refresh
|
22
|
+
return unless expired?
|
23
|
+
now = Time.now.utc
|
24
|
+
|
25
|
+
|
26
|
+
start = now.iso8601
|
27
|
+
@expiration = (now + EXPIRATION)
|
28
|
+
expiry = @expiration.iso8601
|
29
|
+
|
30
|
+
content = <<-XML.gsub!(/[[:space:]]+/, " ").strip!
|
31
|
+
<?xml version="1.0" encoding="utf-8"?>
|
32
|
+
<KeyInfo>
|
33
|
+
<Start>#{start}</Start>
|
34
|
+
<Expiry>#{expiry}</Expiry>
|
35
|
+
</KeyInfo>
|
36
|
+
XML
|
37
|
+
|
38
|
+
response = Http.new(uri, signer:).post(content)
|
39
|
+
|
40
|
+
doc = REXML::Document.new(response)
|
41
|
+
|
42
|
+
@signed_oid = doc.get_elements("/UserDelegationKey/SignedOid").first.get_text.to_s
|
43
|
+
@signed_tid = doc.get_elements("/UserDelegationKey/SignedTid").first.get_text.to_s
|
44
|
+
@signed_start = doc.get_elements("/UserDelegationKey/SignedStart").first.get_text.to_s
|
45
|
+
@signed_expiry = doc.get_elements("/UserDelegationKey/SignedExpiry").first.get_text.to_s
|
46
|
+
@signed_service = doc.get_elements("/UserDelegationKey/SignedService").first.get_text.to_s
|
47
|
+
@signed_version = doc.get_elements("/UserDelegationKey/SignedVersion").first.get_text.to_s
|
48
|
+
@user_delegation_key = Base64.decode64(doc.get_elements("/UserDelegationKey/Value").first.get_text.to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :signed_oid,
|
52
|
+
:signed_tid,
|
53
|
+
:signed_start,
|
54
|
+
:signed_expiry,
|
55
|
+
:signed_service,
|
56
|
+
:signed_version,
|
57
|
+
:user_delegation_key
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def expired?
|
62
|
+
expiration.nil? || Time.now >= (expiration - EXPIRATION_BUFFER)
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :uri, :user_delegation_key, :signer, :expiration
|
66
|
+
end
|
67
|
+
end
|
data/lib/azure_blob/version.rb
CHANGED