azure-blob 0.4.2 → 0.5.0
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/.terraform.lock.hcl +22 -0
- data/CHANGELOG.md +7 -1
- data/README.md +60 -14
- data/Rakefile +40 -4
- data/azure-blob.gemspec +1 -0
- 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 +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
- data/main.tf +187 -0
- data/output.tf +30 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 94edcaaaa7717925c1fb8238b9e7420949b26c98c90344cbf54930d5e0ff3b19
|
4
|
+
data.tar.gz: aae153da753212659590ed4a3ae029c86cf7ed66b4f924dac1a5f3a5ff3b2e05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 102c3d5fd08761199413e96bd8e4bae8a84ac5478d7bb1202e232fbff346703f7cb3b8f344553983b607dbacec56942452ce18aa0afc184a4c994d31dc94118c
|
7
|
+
data.tar.gz: 646636874dd381757595f82999dd0b7b5647a740512c8e2baf86bdc2045e579cdf78354a4690c746452280efb7ff92cbf9c49c95775ccbcf6a690eb0ae05131f
|
data/.terraform.lock.hcl
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# This file is maintained automatically by "terraform init".
|
2
|
+
# Manual edits may be lost in future updates.
|
3
|
+
|
4
|
+
provider "registry.terraform.io/hashicorp/azurerm" {
|
5
|
+
version = "3.113.0"
|
6
|
+
constraints = "~> 3.0"
|
7
|
+
hashes = [
|
8
|
+
"h1:eEUtt0lrLdpVaF6FiDq8BGQPgEcykmhj0aNIL7hTOGw=",
|
9
|
+
"zh:12479f5664288943400447b55e50df675c28ae82ad8d373cc2e5682f3a3411f0",
|
10
|
+
"zh:1b42a14e80e568429d3b55fed753ca3ef0df9dcdfa107890d7264599c020940f",
|
11
|
+
"zh:381be6ca617f848de3baa3985a6e1788e91a803afe04a3c5c727453528b6310d",
|
12
|
+
"zh:3e70e2e07b6db1c363de3e5d0ca47f27fc956473df03329c7d2e54d3ac29176b",
|
13
|
+
"zh:87c7633aeaa828098c6055da9e67d4acaf4b46748b6b3f0267e105e55f05de25",
|
14
|
+
"zh:8d0d98226901f874770dd5220d4701a12ae8bd586994615aa7dcba12b9736bec",
|
15
|
+
"zh:9fd913acd42a60c3a90a18ce803567ef861db8779a59aacced91f2cbd86de9d9",
|
16
|
+
"zh:b6f3f7ae0a055437fb36c139af9bb3135e7f4dad172157ae1eb0177dc74d703f",
|
17
|
+
"zh:b927027ba2bf40d34e03d742fd2b6c5299023b5ab8e6f05e50aac76a46ad1094",
|
18
|
+
"zh:ceb5187b9d2a439f4e48944f3ffeeeaf47a03dbe6f3325ea1775bf659ce0aa88",
|
19
|
+
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
20
|
+
"zh:fb9d78dfeca7489bffca9b1a1f3abee7f16dbbcba31388aea1102062c1d6dce8",
|
21
|
+
]
|
22
|
+
}
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] 2024-09-09
|
4
|
+
|
5
|
+
- Added support for Managed Identities (Entra ID)
|
6
|
+
|
3
7
|
## [0.4.2] 2024-06-06
|
4
8
|
|
5
|
-
Documentation
|
9
|
+
- Documentation
|
10
|
+
- Fix an issue with integrity check on multi block upload
|
11
|
+
|
6
12
|
|
7
13
|
## [0.4.1] 2024-05-27
|
8
14
|
|
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
|
data/azure-blob.gemspec
CHANGED
@@ -13,6 +13,7 @@ Gem::Specification.new do |spec|
|
|
13
13
|
spec.license = "MIT"
|
14
14
|
spec.required_ruby_version = ">= 3.1"
|
15
15
|
|
16
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
16
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
17
18
|
spec.metadata["source_code_uri"] = spec.homepage
|
18
19
|
spec.metadata["changelog_uri"] = "https://github.com/testdouble/azure-blob/blob/main/CHANGELOG.md"
|
data/devenv.lock
CHANGED
@@ -108,16 +108,16 @@
|
|
108
108
|
},
|
109
109
|
"nixpkgs": {
|
110
110
|
"locked": {
|
111
|
-
"lastModified":
|
111
|
+
"lastModified": 1725001927,
|
112
112
|
"owner": "NixOS",
|
113
113
|
"repo": "nixpkgs",
|
114
|
-
"rev": "
|
115
|
-
"treeHash": "
|
114
|
+
"rev": "6e99f2a27d600612004fbd2c3282d614bfee6421",
|
115
|
+
"treeHash": "1e85443cc9f0ba302df2cf61cacb8014943e2d19",
|
116
116
|
"type": "github"
|
117
117
|
},
|
118
118
|
"original": {
|
119
119
|
"owner": "NixOS",
|
120
|
-
"ref": "nixos-
|
120
|
+
"ref": "nixos-24.05",
|
121
121
|
"repo": "nixpkgs",
|
122
122
|
"type": "github"
|
123
123
|
}
|
@@ -129,11 +129,11 @@
|
|
129
129
|
"nixpkgs": "nixpkgs_2"
|
130
130
|
},
|
131
131
|
"locked": {
|
132
|
-
"lastModified":
|
132
|
+
"lastModified": 1724737223,
|
133
133
|
"owner": "bobvanderlinden",
|
134
134
|
"repo": "nixpkgs-ruby",
|
135
|
-
"rev": "
|
136
|
-
"treeHash": "
|
135
|
+
"rev": "175b5867babcbc471b94be9fd5576f2973bbdb6d",
|
136
|
+
"treeHash": "2fe3404ac0eeb7bcb7ac7b5f5f8b9b6a7e460147",
|
137
137
|
"type": "github"
|
138
138
|
},
|
139
139
|
"original": {
|
@@ -160,16 +160,16 @@
|
|
160
160
|
},
|
161
161
|
"nixpkgs_2": {
|
162
162
|
"locked": {
|
163
|
-
"lastModified":
|
163
|
+
"lastModified": 1725001927,
|
164
164
|
"owner": "NixOS",
|
165
165
|
"repo": "nixpkgs",
|
166
|
-
"rev": "
|
167
|
-
"treeHash": "
|
166
|
+
"rev": "6e99f2a27d600612004fbd2c3282d614bfee6421",
|
167
|
+
"treeHash": "1e85443cc9f0ba302df2cf61cacb8014943e2d19",
|
168
168
|
"type": "github"
|
169
169
|
},
|
170
170
|
"original": {
|
171
171
|
"owner": "NixOS",
|
172
|
-
"ref": "nixos-
|
172
|
+
"ref": "nixos-24.05",
|
173
173
|
"repo": "nixpkgs",
|
174
174
|
"type": "github"
|
175
175
|
}
|
data/devenv.nix
CHANGED
@@ -1,12 +1,36 @@
|
|
1
|
-
{ pkgs, ... }:
|
1
|
+
{ pkgs, config, ... }:
|
2
2
|
|
3
3
|
{
|
4
|
+
env = {
|
5
|
+
LD_LIBRARY_PATH = "${config.devenv.profile}/lib";
|
6
|
+
};
|
7
|
+
|
4
8
|
packages = with pkgs; [
|
5
9
|
git
|
6
10
|
libyaml
|
11
|
+
terraform
|
12
|
+
azure-cli
|
13
|
+
glib
|
14
|
+
vips
|
15
|
+
sshuttle
|
16
|
+
sshpass
|
17
|
+
rsync
|
7
18
|
];
|
8
19
|
|
9
|
-
|
10
20
|
languages.ruby.enable = true;
|
11
|
-
languages.ruby.version = "3.1.
|
21
|
+
languages.ruby.version = "3.1.6";
|
22
|
+
|
23
|
+
scripts.generate-env-file.exec = ''
|
24
|
+
terraform output -raw devenv_local_nix > devenv.local.nix
|
25
|
+
'';
|
26
|
+
|
27
|
+
scripts.proxy-vps.exec = ''
|
28
|
+
exec sshuttle -e "ssh -o CheckHostIP=no -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" -r "$(terraform output --raw vm_username)@$(terraform output --raw vm_ip)" 0/0
|
29
|
+
'';
|
30
|
+
|
31
|
+
scripts.start-app-service-ssh.exec = ''
|
32
|
+
resource_group=$(terraform output --raw "resource_group")
|
33
|
+
app_name=$(terraform output --raw "app_service_app_name")
|
34
|
+
exec az webapp create-remote-connection --resource-group $resource_group --name $app_name
|
35
|
+
'';
|
12
36
|
}
|
data/devenv.yaml
CHANGED
data/input.tf
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
variable "location" {
|
2
|
+
type = string
|
3
|
+
default = "westus2"
|
4
|
+
}
|
5
|
+
|
6
|
+
variable "prefix" {
|
7
|
+
type = string
|
8
|
+
default = "azure-blob"
|
9
|
+
}
|
10
|
+
|
11
|
+
variable "storage_account_name" {
|
12
|
+
type = string
|
13
|
+
default = "azureblobrubygemdev"
|
14
|
+
}
|
15
|
+
|
16
|
+
variable "create_vm" {
|
17
|
+
type = bool
|
18
|
+
default = false
|
19
|
+
}
|
20
|
+
|
21
|
+
variable "vm_size" {
|
22
|
+
type = string
|
23
|
+
default = "Standard_B2s"
|
24
|
+
}
|
25
|
+
|
26
|
+
variable "vm_username" {
|
27
|
+
type = string
|
28
|
+
default = "azureblob"
|
29
|
+
}
|
30
|
+
|
31
|
+
variable "vm_password" {
|
32
|
+
type = string
|
33
|
+
default = "qwe123QWE!@#"
|
34
|
+
}
|
35
|
+
|
36
|
+
variable "create_app_service" {
|
37
|
+
type = bool
|
38
|
+
default = false
|
39
|
+
}
|
40
|
+
|
41
|
+
variable "ssh_key" {
|
42
|
+
type = string
|
43
|
+
default = ""
|
44
|
+
}
|
@@ -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
data/main.tf
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
terraform {
|
2
|
+
required_providers {
|
3
|
+
azurerm = {
|
4
|
+
source = "hashicorp/azurerm"
|
5
|
+
version = "~>3.0"
|
6
|
+
}
|
7
|
+
}
|
8
|
+
}
|
9
|
+
|
10
|
+
provider "azurerm" {
|
11
|
+
features {}
|
12
|
+
}
|
13
|
+
|
14
|
+
locals {
|
15
|
+
public_ssh_key = var.ssh_key != "" ? var.ssh_key : file("~/.ssh/id_rsa.pub")
|
16
|
+
}
|
17
|
+
|
18
|
+
resource "azurerm_resource_group" "main" {
|
19
|
+
name = var.prefix
|
20
|
+
location = var.location
|
21
|
+
tags = {
|
22
|
+
source = "Terraform"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
resource "azurerm_storage_account" "main" {
|
27
|
+
name = var.storage_account_name
|
28
|
+
resource_group_name = azurerm_resource_group.main.name
|
29
|
+
location = azurerm_resource_group.main.location
|
30
|
+
account_tier = "Standard"
|
31
|
+
account_replication_type = "LRS"
|
32
|
+
|
33
|
+
tags = {
|
34
|
+
source = "Terraform"
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
resource "azurerm_storage_container" "private" {
|
39
|
+
name = "private"
|
40
|
+
storage_account_name = azurerm_storage_account.main.name
|
41
|
+
container_access_type = "private"
|
42
|
+
}
|
43
|
+
|
44
|
+
resource "azurerm_storage_container" "public" {
|
45
|
+
name = "public"
|
46
|
+
storage_account_name = azurerm_storage_account.main.name
|
47
|
+
container_access_type = "blob"
|
48
|
+
}
|
49
|
+
|
50
|
+
resource "azurerm_virtual_network" "main" {
|
51
|
+
count = var.create_vm ? 1 : 0
|
52
|
+
name = "${var.prefix}-network"
|
53
|
+
address_space = ["10.0.0.0/16"]
|
54
|
+
location = azurerm_resource_group.main.location
|
55
|
+
resource_group_name = azurerm_resource_group.main.name
|
56
|
+
|
57
|
+
tags = {
|
58
|
+
source = "Terraform"
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
resource "azurerm_subnet" "main" {
|
63
|
+
count = var.create_vm ? 1 : 0
|
64
|
+
name = "${var.prefix}-main"
|
65
|
+
resource_group_name = azurerm_resource_group.main.name
|
66
|
+
virtual_network_name = azurerm_virtual_network.main[0].name
|
67
|
+
address_prefixes = ["10.0.2.0/24"]
|
68
|
+
}
|
69
|
+
|
70
|
+
resource "azurerm_network_interface" "main" {
|
71
|
+
count = var.create_vm ? 1 : 0
|
72
|
+
name = "${var.prefix}-nic"
|
73
|
+
location = azurerm_resource_group.main.location
|
74
|
+
resource_group_name = azurerm_resource_group.main.name
|
75
|
+
|
76
|
+
ip_configuration {
|
77
|
+
name = "${var.prefix}-ip-config"
|
78
|
+
subnet_id = azurerm_subnet.main[0].id
|
79
|
+
private_ip_address_allocation = "Dynamic"
|
80
|
+
public_ip_address_id = azurerm_public_ip.main[0].id
|
81
|
+
}
|
82
|
+
|
83
|
+
tags = {
|
84
|
+
source = "Terraform"
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
resource "azurerm_public_ip" "main" {
|
89
|
+
count = var.create_vm ? 1 : 0
|
90
|
+
name = "${var.prefix}-public-ip"
|
91
|
+
resource_group_name = azurerm_resource_group.main.name
|
92
|
+
location = azurerm_resource_group.main.location
|
93
|
+
allocation_method = "Static"
|
94
|
+
|
95
|
+
tags = {
|
96
|
+
source = "Terraform"
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
resource "azurerm_user_assigned_identity" "vm" {
|
101
|
+
location = azurerm_resource_group.main.location
|
102
|
+
name = "${var.prefix}-vm"
|
103
|
+
resource_group_name = azurerm_resource_group.main.name
|
104
|
+
}
|
105
|
+
|
106
|
+
|
107
|
+
resource "azurerm_role_assignment" "vm" {
|
108
|
+
scope = azurerm_storage_account.main.id
|
109
|
+
role_definition_name = "Storage Blob Data Contributor"
|
110
|
+
principal_id = azurerm_user_assigned_identity.vm.principal_id
|
111
|
+
}
|
112
|
+
|
113
|
+
resource "azurerm_linux_virtual_machine" "main" {
|
114
|
+
count = var.create_vm ? 1 : 0
|
115
|
+
name = "${var.prefix}-vm"
|
116
|
+
computer_name = var.prefix
|
117
|
+
resource_group_name = azurerm_resource_group.main.name
|
118
|
+
location = azurerm_resource_group.main.location
|
119
|
+
size = var.vm_size
|
120
|
+
admin_username = var.vm_username
|
121
|
+
admin_password = var.vm_password
|
122
|
+
disable_password_authentication = true
|
123
|
+
network_interface_ids = [azurerm_network_interface.main[0].id]
|
124
|
+
|
125
|
+
identity {
|
126
|
+
type = "UserAssigned"
|
127
|
+
identity_ids = [azurerm_user_assigned_identity.vm.id]
|
128
|
+
}
|
129
|
+
|
130
|
+
admin_ssh_key {
|
131
|
+
username = var.vm_username
|
132
|
+
public_key = local.public_ssh_key
|
133
|
+
}
|
134
|
+
|
135
|
+
source_image_reference {
|
136
|
+
publisher = "Canonical"
|
137
|
+
offer = "0001-com-ubuntu-server-jammy"
|
138
|
+
sku = "22_04-lts"
|
139
|
+
version = "latest"
|
140
|
+
}
|
141
|
+
|
142
|
+
os_disk {
|
143
|
+
caching = "ReadWrite"
|
144
|
+
storage_account_type = "Standard_LRS"
|
145
|
+
}
|
146
|
+
|
147
|
+
tags = {
|
148
|
+
source = "Terraform"
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
resource "azurerm_service_plan" "main" {
|
153
|
+
count = var.create_app_service ? 1 : 0
|
154
|
+
name = "${var.prefix}-appserviceplan"
|
155
|
+
resource_group_name = azurerm_resource_group.main.name
|
156
|
+
location = azurerm_resource_group.main.location
|
157
|
+
os_type = "Linux"
|
158
|
+
sku_name = "B1"
|
159
|
+
}
|
160
|
+
|
161
|
+
resource "azurerm_linux_web_app" "main" {
|
162
|
+
count = var.create_app_service ? 1 : 0
|
163
|
+
name = "${var.prefix}-app"
|
164
|
+
service_plan_id = azurerm_service_plan.main[0].id
|
165
|
+
resource_group_name = azurerm_resource_group.main.name
|
166
|
+
location = azurerm_resource_group.main.location
|
167
|
+
|
168
|
+
identity {
|
169
|
+
type = "UserAssigned"
|
170
|
+
identity_ids = [azurerm_user_assigned_identity.vm.id]
|
171
|
+
}
|
172
|
+
|
173
|
+
site_config {
|
174
|
+
application_stack {
|
175
|
+
node_version = "20-lts"
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
resource "azurerm_app_service_source_control" "main" {
|
181
|
+
count = var.create_app_service ? 1 : 0
|
182
|
+
app_id = azurerm_linux_web_app.main[0].id
|
183
|
+
repo_url = "https://github.com/Azure-Samples/nodejs-docs-hello-world"
|
184
|
+
branch = "master"
|
185
|
+
use_manual_integration = true
|
186
|
+
use_mercurial = false
|
187
|
+
}
|
data/output.tf
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
output "devenv_local_nix" {
|
2
|
+
sensitive = true
|
3
|
+
value = <<EOT
|
4
|
+
{pkgs, lib, ...}:{
|
5
|
+
env = {
|
6
|
+
AZURE_ACCOUNT_NAME = "${azurerm_storage_account.main.name}";
|
7
|
+
AZURE_ACCESS_KEY = "${azurerm_storage_account.main.primary_access_key}";
|
8
|
+
AZURE_PRIVATE_CONTAINER = "${azurerm_storage_container.private.name}";
|
9
|
+
AZURE_PUBLIC_CONTAINER = "${azurerm_storage_container.public.name}";
|
10
|
+
AZURE_PRINCIPAL_ID = "${azurerm_user_assigned_identity.vm.principal_id}";
|
11
|
+
};
|
12
|
+
}
|
13
|
+
EOT
|
14
|
+
}
|
15
|
+
|
16
|
+
output "vm_ip" {
|
17
|
+
value = var.create_vm ? azurerm_public_ip.main[0].ip_address : ""
|
18
|
+
}
|
19
|
+
|
20
|
+
output "vm_username" {
|
21
|
+
value = var.vm_username
|
22
|
+
}
|
23
|
+
|
24
|
+
output "app_service_app_name" {
|
25
|
+
value = var.create_app_service ? azurerm_linux_web_app.main[0].name : ""
|
26
|
+
}
|
27
|
+
|
28
|
+
output "resource_group" {
|
29
|
+
value = azurerm_resource_group.main.name
|
30
|
+
}
|
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.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joé Dupuis
|
@@ -34,6 +34,7 @@ files:
|
|
34
34
|
- ".envrc"
|
35
35
|
- ".rubocop.yml"
|
36
36
|
- ".standard.yml"
|
37
|
+
- ".terraform.lock.hcl"
|
37
38
|
- CHANGELOG.md
|
38
39
|
- LICENSE.txt
|
39
40
|
- README.md
|
@@ -43,6 +44,7 @@ files:
|
|
43
44
|
- devenv.lock
|
44
45
|
- devenv.nix
|
45
46
|
- devenv.yaml
|
47
|
+
- input.tf
|
46
48
|
- lib/active_storage/service/azure_blob_service.rb
|
47
49
|
- lib/azure_blob.rb
|
48
50
|
- lib/azure_blob/blob.rb
|
@@ -52,15 +54,21 @@ files:
|
|
52
54
|
- lib/azure_blob/canonicalized_resource.rb
|
53
55
|
- lib/azure_blob/client.rb
|
54
56
|
- lib/azure_blob/const.rb
|
57
|
+
- lib/azure_blob/entra_id_signer.rb
|
55
58
|
- lib/azure_blob/errors.rb
|
56
59
|
- lib/azure_blob/http.rb
|
60
|
+
- lib/azure_blob/identity_token.rb
|
57
61
|
- lib/azure_blob/metadata.rb
|
58
|
-
- lib/azure_blob/
|
62
|
+
- lib/azure_blob/shared_key_signer.rb
|
63
|
+
- lib/azure_blob/user_delegation_key.rb
|
59
64
|
- lib/azure_blob/version.rb
|
65
|
+
- main.tf
|
66
|
+
- output.tf
|
60
67
|
homepage: https://github.com/testdouble/azure-blob
|
61
68
|
licenses:
|
62
69
|
- MIT
|
63
70
|
metadata:
|
71
|
+
rubygems_mfa_required: 'true'
|
64
72
|
homepage_uri: https://github.com/testdouble/azure-blob
|
65
73
|
source_code_uri: https://github.com/testdouble/azure-blob
|
66
74
|
changelog_uri: https://github.com/testdouble/azure-blob/blob/main/CHANGELOG.md
|