azure-blob 0.4.2 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -1
- data/README.md +60 -14
- data/Rakefile +40 -4
- data/lib/active_storage/service/azure_blob_service.rb +1 -1
- data/lib/azure_blob/client.rb +7 -3
- data/lib/azure_blob/entra_id_signer.rb +115 -0
- data/lib/azure_blob/http.rb +18 -2
- data/lib/azure_blob/identity_token.rb +65 -0
- data/lib/azure_blob/{signer.rb → shared_key_signer.rb} +3 -3
- data/lib/azure_blob/user_delegation_key.rb +67 -0
- data/lib/azure_blob/version.rb +1 -1
- metadata +6 -10
- data/.envrc +0 -3
- data/.rubocop.yml +0 -25
- data/.standard.yml +0 -3
- data/azure-blob.gemspec +0 -33
- data/devenv.local.nix.example +0 -8
- data/devenv.lock +0 -242
- data/devenv.nix +0 -12
- data/devenv.yaml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a83c96b748a5659438b382a79a1af068c1552f4ee895dd16b863b00758d8d1f
|
4
|
+
data.tar.gz: f6def3a97bde3b96ef53618502454b3c3bd0103e07332ed19f817d3298971860
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20f51dbd1ea9661fec78d1371f52459aaf00a4a42e7274f375bd81c7cfc89bac9dfba1546af7cf71f7fea5fd8479fa7ceab9605ea1a8d0ceef7099e0212d4738
|
7
|
+
data.tar.gz: 74939a0d48677079ac46585276369a4d090d8bcbdd4e0ab84d37a8a48d2e54977dec5541b823f0a08c7642787ce13041a54311963010a5449424f13a181e72de
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.1] 2024-09-09
|
4
|
+
|
5
|
+
- Remove dev files from the release
|
6
|
+
|
7
|
+
## [0.5.0] 2024-09-09
|
8
|
+
|
9
|
+
- Added support for Managed Identities (Entra ID)
|
10
|
+
|
3
11
|
## [0.4.2] 2024-06-06
|
4
12
|
|
5
|
-
Documentation
|
13
|
+
- Documentation
|
14
|
+
- Fix an issue with integrity check on multi block upload
|
15
|
+
|
6
16
|
|
7
17
|
## [0.4.1] 2024-05-27
|
8
18
|
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@ This gem was built to replace azure-storage-blob (deprecated) in Active Storage,
|
|
4
4
|
|
5
5
|
## Active Storage
|
6
6
|
|
7
|
-
|
7
|
+
### Migration
|
8
8
|
To migrate from azure-storage-blob to azure-blob:
|
9
9
|
|
10
10
|
1. Replace `azure-storage-blob` in your Gemfile with `azure-blob`
|
@@ -12,6 +12,29 @@ To migrate from azure-storage-blob to azure-blob:
|
|
12
12
|
3. Change the `AzureStorage` service to `AzureBlob` in your Active Storage config (`config/storage.yml`)
|
13
13
|
4. Restart or deploy the app.
|
14
14
|
|
15
|
+
### Managed Identity (Entra ID)
|
16
|
+
|
17
|
+
AzureBlob supports managed identities on :
|
18
|
+
- Azure VM
|
19
|
+
- App Service
|
20
|
+
- Azure Functions (Untested but should work)
|
21
|
+
- Azure Containers (Untested but should work)
|
22
|
+
|
23
|
+
AKS support will likely require more work. Contributions are welcome.
|
24
|
+
|
25
|
+
To authenticate through managed identities instead of a shared key, omit `storage_access_key` from your `storage.yml` file.
|
26
|
+
|
27
|
+
It is recommended to add the identity's `principal_id` to the config.
|
28
|
+
|
29
|
+
ActiveStorage config example:
|
30
|
+
|
31
|
+
```
|
32
|
+
prod:
|
33
|
+
service: AzureBlob
|
34
|
+
container: container_name
|
35
|
+
storage_account_name: account_name
|
36
|
+
principal_id: 71b34410-4c50-451d-b456-95ead1b18cce
|
37
|
+
```
|
15
38
|
|
16
39
|
## Standalone
|
17
40
|
|
@@ -42,6 +65,42 @@ For the full list of methods: https://www.rubydoc.info/gems/azure-blob/AzureBlob
|
|
42
65
|
|
43
66
|
### Dev environment
|
44
67
|
|
68
|
+
A dev environment is supplied through Nix with [devenv](https://devenv.sh/).
|
69
|
+
|
70
|
+
1. Install [devenv](https://devenv.sh/).
|
71
|
+
2. Enter the dev environment by cd into the repo and running `devenv shell` (or `direnv allow` if you are a direnv user).
|
72
|
+
3. Log into azure CLI with `az login`
|
73
|
+
4. `terraform init`
|
74
|
+
5. `terraform apply` This will generate the necessary infrastructure on azure.
|
75
|
+
6. Generate devenv.local.nix with your private key and container information: `generate-env-file`
|
76
|
+
7. If you are using direnv, the environment will reload automatically. If not, exit the shell and reopen it by hitting <C-d> and running `devenv shell` again.
|
77
|
+
|
78
|
+
#### Entra ID
|
79
|
+
|
80
|
+
To test with Entra ID, the `AZURE_ACCESS_KEY` environment variable must be unset and the code must be ran or proxied through a VPS with the proper roles.
|
81
|
+
|
82
|
+
For cost saving, the terraform variable `create_vm` is false by default.
|
83
|
+
To create the VPS, Create a var file `var.tfvars` containing:
|
84
|
+
|
85
|
+
```
|
86
|
+
create_vm = true
|
87
|
+
```
|
88
|
+
and re-apply terraform: `terraform apply -var-file=var.tfvars`.
|
89
|
+
|
90
|
+
This will create the VPS and required managed identities.
|
91
|
+
|
92
|
+
`bin/rake test_azure_vm` and `bin/rake test_app_service` will establish a VPN connection to the VM or App service container and run the test suite. You might be prompted for a sudo password when the VPN starts (sshuttle).
|
93
|
+
|
94
|
+
After you are done, run terraform again without the var file (`terraform apply`) to destroy the VPS and App service application.
|
95
|
+
|
96
|
+
#### Cleanup
|
97
|
+
|
98
|
+
Some tests copied over from Rails don't clean after themselves. A rake task is provided to empty your containers and keep cost low: `bin/rake flush_test_container`
|
99
|
+
|
100
|
+
#### Run without devenv/nix
|
101
|
+
|
102
|
+
If you prefer not using devenv/nix:
|
103
|
+
|
45
104
|
Ensure your version of Ruby fit the minimum version in `azure-blob.gemspec`
|
46
105
|
|
47
106
|
and setup those Env variables:
|
@@ -51,19 +110,6 @@ and setup those Env variables:
|
|
51
110
|
- `AZURE_PRIVATE_CONTAINER`
|
52
111
|
- `AZURE_PUBLIC_CONTAINER`
|
53
112
|
|
54
|
-
|
55
|
-
A dev environment setup is also supplied through Nix with [devenv](https://devenv.sh/).
|
56
|
-
|
57
|
-
To use the Nix environment:
|
58
|
-
1. install [devenv](https://devenv.sh/)
|
59
|
-
2. Copy `devenv.local.nix.example` to `devenv.local.nix`
|
60
|
-
3. Insert your azure credentials into `devenv.local.nix`
|
61
|
-
4. Start the shell with `devenv shell` or with [direnv](https://direnv.net/).
|
62
|
-
|
63
|
-
### Tests
|
64
|
-
|
65
|
-
`bin/rake test`
|
66
|
-
|
67
113
|
## License
|
68
114
|
|
69
115
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -2,21 +2,57 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "minitest/test_task"
|
5
|
-
require
|
5
|
+
require "azure_blob"
|
6
|
+
require_relative "test/support/app_service_vpn"
|
7
|
+
require_relative "test/support/azure_vm_vpn"
|
6
8
|
|
7
|
-
Minitest::TestTask.create
|
9
|
+
Minitest::TestTask.create(:test_rails) do
|
10
|
+
self.test_globs = [ "test/rails/**/test_*.rb",
|
11
|
+
"test/rails/**/*_test.rb", ]
|
12
|
+
end
|
13
|
+
|
14
|
+
Minitest::TestTask.create(:test_client) do
|
15
|
+
self.test_globs = [ "test/client/**/test_*.rb",
|
16
|
+
"test/client/**/*_test.rb", ]
|
17
|
+
end
|
8
18
|
|
9
19
|
task default: %i[test]
|
10
20
|
|
21
|
+
task :test do
|
22
|
+
Rake::Task["test_client"].execute
|
23
|
+
Rake::Task["test_rails"].execute
|
24
|
+
end
|
25
|
+
|
26
|
+
task :test_app_service do |t|
|
27
|
+
vpn = AppServiceVpn.new
|
28
|
+
ENV["IDENTITY_ENDPOINT"] = vpn.endpoint
|
29
|
+
ENV["IDENTITY_HEADER"] = vpn.header
|
30
|
+
Rake::Task["test_entra_id"].execute
|
31
|
+
ensure
|
32
|
+
vpn.kill
|
33
|
+
end
|
34
|
+
|
35
|
+
task :test_azure_vm do |t|
|
36
|
+
vpn = AzureVmVpn.new
|
37
|
+
Rake::Task["test_entra_id"].execute
|
38
|
+
ensure
|
39
|
+
vpn.kill
|
40
|
+
end
|
41
|
+
|
42
|
+
task :test_entra_id do |t|
|
43
|
+
ENV["AZURE_ACCESS_KEY"] = nil
|
44
|
+
Rake::Task["test"].execute
|
45
|
+
end
|
46
|
+
|
11
47
|
task :flush_test_container do |t|
|
12
48
|
AzureBlob::Client.new(
|
13
49
|
account_name: ENV["AZURE_ACCOUNT_NAME"],
|
14
50
|
access_key: ENV["AZURE_ACCESS_KEY"],
|
15
51
|
container: ENV["AZURE_PRIVATE_CONTAINER"],
|
16
|
-
).delete_prefix
|
52
|
+
).delete_prefix ""
|
17
53
|
AzureBlob::Client.new(
|
18
54
|
account_name: ENV["AZURE_ACCOUNT_NAME"],
|
19
55
|
access_key: ENV["AZURE_ACCESS_KEY"],
|
20
56
|
container: ENV["AZURE_PUBLIC_CONTAINER"],
|
21
|
-
).delete_prefix
|
57
|
+
).delete_prefix ""
|
22
58
|
end
|
@@ -35,7 +35,7 @@ module ActiveStorage
|
|
35
35
|
class Service::AzureBlobService < Service
|
36
36
|
attr_reader :client, :container, :signer
|
37
37
|
|
38
|
-
def initialize(storage_account_name:, storage_access_key
|
38
|
+
def initialize(storage_account_name:, storage_access_key: nil, container:, public: false, **options)
|
39
39
|
@container = container
|
40
40
|
@public = public
|
41
41
|
@client = AzureBlob::Client.new(
|
data/lib/azure_blob/client.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
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
|
|
@@ -12,10 +13,13 @@ module AzureBlob
|
|
12
13
|
# AzureBlob Client class. You interact with the Azure Blob api
|
13
14
|
# through an instance of this class.
|
14
15
|
class Client
|
15
|
-
def initialize(account_name:, access_key:, container
|
16
|
+
def initialize(account_name:, access_key:, container:, **options)
|
16
17
|
@account_name = account_name
|
17
18
|
@container = container
|
18
|
-
|
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))
|
19
23
|
end
|
20
24
|
|
21
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.
|
@@ -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
@@ -7,7 +7,14 @@ require "rexml"
|
|
7
7
|
|
8
8
|
module AzureBlob
|
9
9
|
class Http # :nodoc:
|
10
|
-
class Error < AzureBlob::Error
|
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
|
@@ -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,12 +71,12 @@ 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
|
@@ -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
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: azure-blob
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joé Dupuis
|
@@ -31,18 +31,10 @@ executables: []
|
|
31
31
|
extensions: []
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
|
-
- ".envrc"
|
35
|
-
- ".rubocop.yml"
|
36
|
-
- ".standard.yml"
|
37
34
|
- CHANGELOG.md
|
38
35
|
- LICENSE.txt
|
39
36
|
- README.md
|
40
37
|
- Rakefile
|
41
|
-
- azure-blob.gemspec
|
42
|
-
- devenv.local.nix.example
|
43
|
-
- devenv.lock
|
44
|
-
- devenv.nix
|
45
|
-
- devenv.yaml
|
46
38
|
- lib/active_storage/service/azure_blob_service.rb
|
47
39
|
- lib/azure_blob.rb
|
48
40
|
- lib/azure_blob/blob.rb
|
@@ -52,15 +44,19 @@ files:
|
|
52
44
|
- lib/azure_blob/canonicalized_resource.rb
|
53
45
|
- lib/azure_blob/client.rb
|
54
46
|
- lib/azure_blob/const.rb
|
47
|
+
- lib/azure_blob/entra_id_signer.rb
|
55
48
|
- lib/azure_blob/errors.rb
|
56
49
|
- lib/azure_blob/http.rb
|
50
|
+
- lib/azure_blob/identity_token.rb
|
57
51
|
- lib/azure_blob/metadata.rb
|
58
|
-
- lib/azure_blob/
|
52
|
+
- lib/azure_blob/shared_key_signer.rb
|
53
|
+
- lib/azure_blob/user_delegation_key.rb
|
59
54
|
- lib/azure_blob/version.rb
|
60
55
|
homepage: https://github.com/testdouble/azure-blob
|
61
56
|
licenses:
|
62
57
|
- MIT
|
63
58
|
metadata:
|
59
|
+
rubygems_mfa_required: 'true'
|
64
60
|
homepage_uri: https://github.com/testdouble/azure-blob
|
65
61
|
source_code_uri: https://github.com/testdouble/azure-blob
|
66
62
|
changelog_uri: https://github.com/testdouble/azure-blob/blob/main/CHANGELOG.md
|
data/.envrc
DELETED
data/.rubocop.yml
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
AllCops:
|
2
|
-
TargetRubyVersion: 3.1
|
3
|
-
|
4
|
-
# Omakase Ruby styling for Rails
|
5
|
-
inherit_gem: { rubocop-rails-omakase: rubocop.yml }
|
6
|
-
|
7
|
-
# Overwrite or add rules to create your own house style
|
8
|
-
#
|
9
|
-
# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
|
10
|
-
# Layout/SpaceInsideArrayLiteralBrackets:
|
11
|
-
# Enabled: false
|
12
|
-
|
13
|
-
Style/TrailingCommaInArrayLiteral:
|
14
|
-
Enabled: true
|
15
|
-
EnforcedStyleForMultiline: consistent_comma
|
16
|
-
|
17
|
-
Style/TrailingCommaInHashLiteral:
|
18
|
-
Enabled: true
|
19
|
-
EnforcedStyleForMultiline: consistent_comma
|
20
|
-
|
21
|
-
|
22
|
-
Rails/AssertNot:
|
23
|
-
Enabled: false
|
24
|
-
Rails/RefuteMethods:
|
25
|
-
Enabled: false
|
data/.standard.yml
DELETED
data/azure-blob.gemspec
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "lib/azure_blob/version"
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = "azure-blob"
|
7
|
-
spec.version = AzureBlob::VERSION
|
8
|
-
spec.authors = [ "Joé Dupuis" ]
|
9
|
-
spec.email = [ "joe@dupuis.io" ]
|
10
|
-
|
11
|
-
spec.summary = "Azure blob client"
|
12
|
-
spec.homepage = "https://github.com/testdouble/azure-blob"
|
13
|
-
spec.license = "MIT"
|
14
|
-
spec.required_ruby_version = ">= 3.1"
|
15
|
-
|
16
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/testdouble/azure-blob/blob/main/CHANGELOG.md"
|
19
|
-
|
20
|
-
spec.add_dependency "rexml"
|
21
|
-
|
22
|
-
# Specify which files should be added to the gem when it is released.
|
23
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
-
spec.files = Dir.chdir(__dir__) do
|
25
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
-
(File.expand_path(f) == __FILE__) ||
|
27
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
28
|
-
end
|
29
|
-
end
|
30
|
-
spec.bindir = "exe"
|
31
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
-
spec.require_paths = [ "lib" ]
|
33
|
-
end
|
data/devenv.local.nix.example
DELETED
data/devenv.lock
DELETED
@@ -1,242 +0,0 @@
|
|
1
|
-
{
|
2
|
-
"nodes": {
|
3
|
-
"devenv": {
|
4
|
-
"locked": {
|
5
|
-
"dir": "src/modules",
|
6
|
-
"lastModified": 1715593316,
|
7
|
-
"narHash": "sha256-S7XatU9uV3q9bVBcg/ER0VMQcnPZprrVlN209ne7LDw=",
|
8
|
-
"owner": "cachix",
|
9
|
-
"repo": "devenv",
|
10
|
-
"rev": "725c90407ef53cc2a1b53701c6d2d0745cf2484f",
|
11
|
-
"type": "github"
|
12
|
-
},
|
13
|
-
"original": {
|
14
|
-
"dir": "src/modules",
|
15
|
-
"owner": "cachix",
|
16
|
-
"repo": "devenv",
|
17
|
-
"type": "github"
|
18
|
-
}
|
19
|
-
},
|
20
|
-
"flake-compat": {
|
21
|
-
"flake": false,
|
22
|
-
"locked": {
|
23
|
-
"lastModified": 1696426674,
|
24
|
-
"owner": "edolstra",
|
25
|
-
"repo": "flake-compat",
|
26
|
-
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
27
|
-
"treeHash": "2addb7b71a20a25ea74feeaf5c2f6a6b30898ecb",
|
28
|
-
"type": "github"
|
29
|
-
},
|
30
|
-
"original": {
|
31
|
-
"owner": "edolstra",
|
32
|
-
"repo": "flake-compat",
|
33
|
-
"type": "github"
|
34
|
-
}
|
35
|
-
},
|
36
|
-
"flake-compat_2": {
|
37
|
-
"flake": false,
|
38
|
-
"locked": {
|
39
|
-
"lastModified": 1696426674,
|
40
|
-
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
41
|
-
"owner": "edolstra",
|
42
|
-
"repo": "flake-compat",
|
43
|
-
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
44
|
-
"type": "github"
|
45
|
-
},
|
46
|
-
"original": {
|
47
|
-
"owner": "edolstra",
|
48
|
-
"repo": "flake-compat",
|
49
|
-
"type": "github"
|
50
|
-
}
|
51
|
-
},
|
52
|
-
"flake-utils": {
|
53
|
-
"inputs": {
|
54
|
-
"systems": "systems"
|
55
|
-
},
|
56
|
-
"locked": {
|
57
|
-
"lastModified": 1710146030,
|
58
|
-
"owner": "numtide",
|
59
|
-
"repo": "flake-utils",
|
60
|
-
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
61
|
-
"treeHash": "bd263f021e345cb4a39d80c126ab650bebc3c10c",
|
62
|
-
"type": "github"
|
63
|
-
},
|
64
|
-
"original": {
|
65
|
-
"owner": "numtide",
|
66
|
-
"repo": "flake-utils",
|
67
|
-
"type": "github"
|
68
|
-
}
|
69
|
-
},
|
70
|
-
"flake-utils_2": {
|
71
|
-
"inputs": {
|
72
|
-
"systems": "systems_2"
|
73
|
-
},
|
74
|
-
"locked": {
|
75
|
-
"lastModified": 1710146030,
|
76
|
-
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
77
|
-
"owner": "numtide",
|
78
|
-
"repo": "flake-utils",
|
79
|
-
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
80
|
-
"type": "github"
|
81
|
-
},
|
82
|
-
"original": {
|
83
|
-
"owner": "numtide",
|
84
|
-
"repo": "flake-utils",
|
85
|
-
"type": "github"
|
86
|
-
}
|
87
|
-
},
|
88
|
-
"gitignore": {
|
89
|
-
"inputs": {
|
90
|
-
"nixpkgs": [
|
91
|
-
"pre-commit-hooks",
|
92
|
-
"nixpkgs"
|
93
|
-
]
|
94
|
-
},
|
95
|
-
"locked": {
|
96
|
-
"lastModified": 1709087332,
|
97
|
-
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
98
|
-
"owner": "hercules-ci",
|
99
|
-
"repo": "gitignore.nix",
|
100
|
-
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
101
|
-
"type": "github"
|
102
|
-
},
|
103
|
-
"original": {
|
104
|
-
"owner": "hercules-ci",
|
105
|
-
"repo": "gitignore.nix",
|
106
|
-
"type": "github"
|
107
|
-
}
|
108
|
-
},
|
109
|
-
"nixpkgs": {
|
110
|
-
"locked": {
|
111
|
-
"lastModified": 1715542476,
|
112
|
-
"owner": "NixOS",
|
113
|
-
"repo": "nixpkgs",
|
114
|
-
"rev": "44072e24566c5bcc0b7aa9178a0104f4cfffab19",
|
115
|
-
"treeHash": "3f9021e4c33de6fe59b88ac8c3019fc49136dc2a",
|
116
|
-
"type": "github"
|
117
|
-
},
|
118
|
-
"original": {
|
119
|
-
"owner": "NixOS",
|
120
|
-
"ref": "nixos-23.11",
|
121
|
-
"repo": "nixpkgs",
|
122
|
-
"type": "github"
|
123
|
-
}
|
124
|
-
},
|
125
|
-
"nixpkgs-ruby": {
|
126
|
-
"inputs": {
|
127
|
-
"flake-compat": "flake-compat",
|
128
|
-
"flake-utils": "flake-utils",
|
129
|
-
"nixpkgs": "nixpkgs_2"
|
130
|
-
},
|
131
|
-
"locked": {
|
132
|
-
"lastModified": 1713939467,
|
133
|
-
"owner": "bobvanderlinden",
|
134
|
-
"repo": "nixpkgs-ruby",
|
135
|
-
"rev": "c1ba161adf31119cfdbb24489766a7bcd4dbe881",
|
136
|
-
"treeHash": "0d32620317b29f94d6718684f030dd2fc2f30cb2",
|
137
|
-
"type": "github"
|
138
|
-
},
|
139
|
-
"original": {
|
140
|
-
"owner": "bobvanderlinden",
|
141
|
-
"repo": "nixpkgs-ruby",
|
142
|
-
"type": "github"
|
143
|
-
}
|
144
|
-
},
|
145
|
-
"nixpkgs-stable": {
|
146
|
-
"locked": {
|
147
|
-
"lastModified": 1710695816,
|
148
|
-
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
|
149
|
-
"owner": "NixOS",
|
150
|
-
"repo": "nixpkgs",
|
151
|
-
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
|
152
|
-
"type": "github"
|
153
|
-
},
|
154
|
-
"original": {
|
155
|
-
"owner": "NixOS",
|
156
|
-
"ref": "nixos-23.11",
|
157
|
-
"repo": "nixpkgs",
|
158
|
-
"type": "github"
|
159
|
-
}
|
160
|
-
},
|
161
|
-
"nixpkgs_2": {
|
162
|
-
"locked": {
|
163
|
-
"lastModified": 1715542476,
|
164
|
-
"owner": "NixOS",
|
165
|
-
"repo": "nixpkgs",
|
166
|
-
"rev": "44072e24566c5bcc0b7aa9178a0104f4cfffab19",
|
167
|
-
"treeHash": "3f9021e4c33de6fe59b88ac8c3019fc49136dc2a",
|
168
|
-
"type": "github"
|
169
|
-
},
|
170
|
-
"original": {
|
171
|
-
"owner": "NixOS",
|
172
|
-
"ref": "nixos-23.11",
|
173
|
-
"repo": "nixpkgs",
|
174
|
-
"type": "github"
|
175
|
-
}
|
176
|
-
},
|
177
|
-
"pre-commit-hooks": {
|
178
|
-
"inputs": {
|
179
|
-
"flake-compat": "flake-compat_2",
|
180
|
-
"flake-utils": "flake-utils_2",
|
181
|
-
"gitignore": "gitignore",
|
182
|
-
"nixpkgs": [
|
183
|
-
"nixpkgs"
|
184
|
-
],
|
185
|
-
"nixpkgs-stable": "nixpkgs-stable"
|
186
|
-
},
|
187
|
-
"locked": {
|
188
|
-
"lastModified": 1715609711,
|
189
|
-
"narHash": "sha256-/5u29K0c+4jyQ8x7dUIEUWlz2BoTSZWUP2quPwFCE7M=",
|
190
|
-
"owner": "cachix",
|
191
|
-
"repo": "pre-commit-hooks.nix",
|
192
|
-
"rev": "c182c876690380f8d3b9557c4609472ebfa1b141",
|
193
|
-
"type": "github"
|
194
|
-
},
|
195
|
-
"original": {
|
196
|
-
"owner": "cachix",
|
197
|
-
"repo": "pre-commit-hooks.nix",
|
198
|
-
"type": "github"
|
199
|
-
}
|
200
|
-
},
|
201
|
-
"root": {
|
202
|
-
"inputs": {
|
203
|
-
"devenv": "devenv",
|
204
|
-
"nixpkgs": "nixpkgs",
|
205
|
-
"nixpkgs-ruby": "nixpkgs-ruby",
|
206
|
-
"pre-commit-hooks": "pre-commit-hooks"
|
207
|
-
}
|
208
|
-
},
|
209
|
-
"systems": {
|
210
|
-
"locked": {
|
211
|
-
"lastModified": 1681028828,
|
212
|
-
"owner": "nix-systems",
|
213
|
-
"repo": "default",
|
214
|
-
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
215
|
-
"treeHash": "cce81f2a0f0743b2eb61bc2eb6c7adbe2f2c6beb",
|
216
|
-
"type": "github"
|
217
|
-
},
|
218
|
-
"original": {
|
219
|
-
"owner": "nix-systems",
|
220
|
-
"repo": "default",
|
221
|
-
"type": "github"
|
222
|
-
}
|
223
|
-
},
|
224
|
-
"systems_2": {
|
225
|
-
"locked": {
|
226
|
-
"lastModified": 1681028828,
|
227
|
-
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
228
|
-
"owner": "nix-systems",
|
229
|
-
"repo": "default",
|
230
|
-
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
231
|
-
"type": "github"
|
232
|
-
},
|
233
|
-
"original": {
|
234
|
-
"owner": "nix-systems",
|
235
|
-
"repo": "default",
|
236
|
-
"type": "github"
|
237
|
-
}
|
238
|
-
}
|
239
|
-
},
|
240
|
-
"root": "root",
|
241
|
-
"version": 7
|
242
|
-
}
|
data/devenv.nix
DELETED