If you use Azure block storage, chances are you want to take advantage of a shared access signature (SAS), a feature usually known as pre-signing URLs. Shared access signatures allow you to share access to your blobs with a given time limit.
The Elixir project I am on is hosted on Azure and so it’s to nobody surprise that we are using the Azure Blob Storage for our uploads. We started with ExAzure, a wrapper for Erlang’s :erlazure
, to do the uploads from Elixir.
However, when it comes to pre-signing, we are on our own.
I am including an example of a minimal module that can pre-sign URLs, but make sure you go through and understand the documentation on SAS and official examples.
Below is a MyApp.Storage.Blob
module that can pre-sign given paths using the shared access signature.
As a configuration you will need to provide a hostname
, account_name
and access_key
:
defmodule MyApp.Storage.Blob do
@hostname Application.get_env(:digi, MyApp.Storage)[:hostname]
@account_name Application.get_env(:digi, MyApp.Storage)[:account_name]
@access_key Application.get_env(:digi, MyApp.Storage)[:access_key]
...
Thesigned_uri/2
function is the main function of the module. It accepts the path to sign and options.
It gives read-only access for fixed 30 minutes by default, but options let you adjust permissions and exact start and expiry datetimes:
...
def signed_uri(path, options \\ %{}) do
now = DateTime.truncate(DateTime.utc_now(), :second)
expiry = Timex.shift(now, minutes: +30)
default_options = %{
path: path,
permissions: "r",
start: DateTime.to_iso8601(now),
expiry: DateTime.to_iso8601(expiry),
stg_version: "2017-11-09",
protocol: ""
}
opts = Map.merge(default_options, options)
signable_string = signable_string_for_blob(opts)
signature = sign(signable_string)
query_hash = %{
"sp" => opts[:permissions],
"sv" => opts[:stg_version],
"sr" => "b",
"st" => opts[:start],
"se" => opts[:expiry],
"sig" => signature
}
"https://#{@hostname}/#{path}?" <> URI.encode_query(query_hash)
end
...
As you can see, the main purpose is to build an actual URI.
We also needed to prepare a signable string using signable_string_for_blob/1
and generate HMAC signature with sign/1
functions:
...
defp sign(body) do
{:ok, key} = @access_key |> Base.decode64()
:crypto.hmac(:sha256, key, body)
|> Base.encode64()
end
defp signable_string_for_blob(options) do
signable_opts = [
options[:permissions],
options[:start],
options[:expiry],
"/blob/#{@account_name}/#{options[:path]}",
options[:identifier],
options[:ip_range],
options[:protocol],
options[:stg_version],
options[:cache_control],
options[:content_disposition],
options[:content_encoding],
options[:content_language],
options[:content_type]
]
Enum.join(signable_opts, "\n")
end
end
And that’s pretty much everything to get you started.
The usage of the signed_uri/2
function is simple (paths depend on your storage):
...
def get_tmp_url(id) do
MyApp.Storage.Blob.signed_uri("#{@container}/tmp/#{id}.png")
end
...