Codeunit 9064 Stor. Serv. Auth. Shared Key, source in 26
Source242526272829metadata in 26
src/System Application/App/Azure Storage Services Authorization/src/SharedKey/StorServAuthSharedKey.Codeunit.al211 lines, Copyright (c) Microsoft Corporation. MIT
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace System.Azure.Storage;
using System;
using System.Utilities;
/// <summary>
/// Exposes functionality to handle the creation of a signature to sign requests to the Storage Services REST API
/// More Information:
/// </summary>
codeunit 9064 "Stor. Serv. Auth. Shared Key" implements "Storage Service Authorization"
{
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;
procedure Authorize(var HttpRequest: HttpRequestMessage; StorageAccount: Text)
var
Headers: HttpHeaders;
begin
HttpRequest.GetHeaders(Headers);
if Headers.Contains('Authorization') then
Headers.Remove('Authorization');
Headers.Add('Authorization', GetSharedKeySignature(HttpRequest, StorageAccount));
end;
procedure SetSharedKey(SharedKey: SecretText)
begin
Secret := SharedKey;
end;
procedure SetApiVersion(NewApiVersion: Enum "Storage Service API Version")
begin
ApiVersion := NewApiVersion;
end;
[NonDebuggable]
local procedure GetSharedKeySignature(HttpRequestMessage: HttpRequestMessage; StorageAccount: Text): Text
var
StringToSign: Text;
Signature: Text;
SignaturePlaceHolderLbl: Label 'SharedKey %1:%2', Comment = '%1 = Account Name; %2 = Calculated Signature', Locked = true;
SecretCanNotBeEmptyErr: Label 'Secret (Access Key) must be provided';
begin
if Secret.IsEmpty() then
Error(SecretCanNotBeEmptyErr);
StringToSign := CreateSharedKeyStringToSign(HttpRequestMessage, StorageAccount);
Signature := AuthFormatHelper.GetAccessKeyHashCode(StringToSign, Secret);
exit(StrSubstNo(SignaturePlaceHolderLbl, StorageAccount, Signature));
end;
local procedure CreateSharedKeyStringToSign(HttpRequestMessage: HttpRequestMessage; StorageAccount: Text): Text
var
RequestHeaders, ContentHeaders : HttpHeaders;
StringToSign: Text;
begin
HttpRequestMessage.GetHeaders(RequestHeaders);
StringToSign += HttpRequestMessage.Method() + NewLine();
if TryGetContentHeaders(HttpRequestMessage, ContentHeaders) then begin
StringToSign += GetHeaderValueOrEmpty(ContentHeaders, 'Content-Encoding') + NewLine();
StringToSign += GetHeaderValueOrEmpty(ContentHeaders, 'Content-Language') + NewLine();
StringToSign += GetHeaderValueOrEmpty(ContentHeaders, 'Content-Length') + NewLine();
StringToSign += GetHeaderValueOrEmpty(ContentHeaders, 'Content-MD5') + NewLine();
StringToSign += GetHeaderValueOrEmpty(ContentHeaders, 'Content-Type') + NewLine();
end else
StringToSign += NewLine() + NewLine() + NewLine() + NewLine() + NewLine();
StringToSign += GetHeaderValueOrEmpty(RequestHeaders, 'Date') + NewLine();
StringToSign += GetHeaderValueOrEmpty(RequestHeaders, 'If-Modified-Since') + NewLine();
StringToSign += GetHeaderValueOrEmpty(RequestHeaders, 'If-Match') + NewLine();
StringToSign += GetHeaderValueOrEmpty(RequestHeaders, 'If-None-Match') + NewLine();
StringToSign += GetHeaderValueOrEmpty(RequestHeaders, 'If-Unmodified-Since') + NewLine();
StringToSign += GetHeaderValueOrEmpty(RequestHeaders, 'Range') + NewLine();
StringToSign += GetCanonicalizedHeaders(RequestHeaders) + NewLine();
StringToSign += GetCanonicalizedResource(StorageAccount, HttpRequestMessage.GetRequestUri());
exit(StringToSign);
end;
[TryFunction]
local procedure TryGetContentHeaders(var HttpRequestMessage: HttpRequestMessage; var RequestHttpHeaders: HttpHeaders)
begin
HttpRequestMessage.Content.GetHeaders(RequestHttpHeaders);
end;
local procedure GetHeaderValueOrEmpty(Headers: HttpHeaders; HeaderKey: Text): Text
var
ReturnValue: array[1] of Text;
begin
if not Headers.Contains(HeaderKey) then
exit('');
if not Headers.GetValues(HeaderKey, ReturnValue) then
exit('');
if HeaderKey = 'Content-Length' then
if ReturnValue[1] = '0' then
exit('');
exit(ReturnValue[1]);
end;
// see https://go.microsoft.com/fwlink/?linkid=2211418
local procedure GetCanonicalizedHeaders(Headers: HttpHeaders): Text
var
HeaderKey: Text;
HeaderValue: array[1] of Text;
CanonicalizedHeaders: Text;
KeyValuePairLbl: Label '%1:%2', Comment = '%1 = Key; %2 = Value', Locked = true;
begin
foreach HeaderKey in Headers.Keys() do
if HeaderKey.StartsWith('x-ms-') then
if Headers.Contains(HeaderKey) then
if Headers.GetValues(HeaderKey, HeaderValue) then begin
if CanonicalizedHeaders <> '' then
CanonicalizedHeaders += NewLine();
CanonicalizedHeaders += StrSubstNo(KeyValuePairLbl, HeaderKey.ToLower(), HeaderValue[1])
end;
exit(CanonicalizedHeaders);
end;
local procedure GetCanonicalizedResource(StorageAccount: Text; UriString: Text): Text
var
Uri: Codeunit Uri;
UriBuilder: Codeunit "Uri Builder";
SortedDictionaryQuery: DotNet GenericSortedDictionary2;
SortedDictionaryEntry: DotNet GenericKeyValuePair2;
QueryString: Text;
Segments: List of [Text];
Segment: Text;
StringBuilderResource: TextBuilder;
StringBuilderQuery: TextBuilder;
StringBuilderCanonicalizedResource: TextBuilder;
KeyValuePairLbl: Label '%1:%2', Comment = '%1 = Key; %2 = Value', Locked = true;
begin
Uri.Init(UriString);
Uri.GetSegments(Segments);
UriBuilder.Init(UriString);
QueryString := UriBuilder.GetQuery();
StringBuilderResource.Append('/');
StringBuilderResource.Append(StorageAccount);
foreach Segment in Segments do
StringBuilderResource.Append(Segment);
if QueryString <> '' then begin
// According to documentation it should be lexicographically, but I didn't find a better way than SortedDictionary
// see: https://go.microsoft.com/fwlink/?linkid=2211418
SplitQueryStringIntoSortedDictionary(QueryString, SortedDictionaryQuery);
foreach SortedDictionaryEntry in SortedDictionaryQuery do begin
StringBuilderQuery.Append(NewLine());
StringBuilderQuery.Append(StrSubstNo(KeyValuePairLbl, SortedDictionaryEntry."Key", Uri.UnescapeDataString(SortedDictionaryEntry.Value)));
end;
end;
StringBuilderCanonicalizedResource.Append(StringBuilderResource.ToText());
StringBuilderCanonicalizedResource.Append(StringBuilderQuery.ToText());
exit(StringBuilderCanonicalizedResource.ToText());
end;
local procedure SplitQueryStringIntoSortedDictionary(QueryString: Text; var NewSortedDictionary: DotNet GenericSortedDictionary2)
var
Segments: List of [Text];
Segment: Text;
CurrIdentifier: Text;
CurrValue: Text;
begin
if QueryString.StartsWith('?') then
QueryString := CopyStr(QueryString, 2);
Segments := QueryString.Split('&');
NewSortedDictionary := NewSortedDictionary.SortedDictionary();
foreach Segment in Segments do
if GetKeyValueFromQueryParameter(Segment, CurrIdentifier, CurrValue) then
NewSortedDictionary.Add(CurrIdentifier, CurrValue);
end;
local procedure GetKeyValueFromQueryParameter(QueryString: Text; var CurrIdentifier: Text; var CurrValue: Text): Boolean
var
Split: List of [Text];
begin
Split := QueryString.Split('=');
if Split.Count() <> 2 then
exit(false); // This should not happen
CurrIdentifier := Split.Get(1);
CurrValue := Split.Get(2);
exit(true);
end;
local procedure NewLine(): Text
begin
exit(AuthFormatHelper.NewLine());
end;
var
AuthFormatHelper: Codeunit "Auth. Format Helper";
ApiVersion: Enum "Storage Service API Version";
Secret: SecretText;
}