azure-blob 0.4.2 → 0.5.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/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