azure-blob 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.terraform.lock.hcl +22 -0
- data/CHANGELOG.md +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
|