Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add User Delegation Credential and User Delgation SAS #18938

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 137 additions & 0 deletions sdk/storage/azblob/zc_sas_service.go
Expand Up @@ -184,6 +184,143 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(sharedKeyCredential *Share
return p, nil
}

// NewSASQueryParametersWithUserDelegation uses an account's UserDelegationCredential to sign this signature values to produce
// the proper SAS query parameters.
func (v BlobSASSignatureValues) NewSASQueryParametersWithUserDelegation(credential *UserDelegationCredential) (SASQueryParameters, error) {
resource := "c"
if credential == nil {
return SASQueryParameters{}, fmt.Errorf("cannot sign SAS query without Shared Key Credential")
}

if !v.SnapshotTime.IsZero() {
resource = "bs"
//Make sure the permission characters are in the correct order
perms := &BlobSASPermissions{}
if err := perms.Parse(v.Permissions); err != nil {
return SASQueryParameters{}, err
}
v.Permissions = perms.String()
} else if v.BlobVersion != "" {
resource = "bv"
//Make sure the permission characters are in the correct order
perms := &BlobSASPermissions{}
if err := perms.Parse(v.Permissions); err != nil {
return SASQueryParameters{}, err
}
v.Permissions = perms.String()
} else if v.Directory != "" {
resource = "d"
v.BlobName = ""
perms := &BlobSASPermissions{}
if err := perms.Parse(v.Permissions); err != nil {
return SASQueryParameters{}, err
}
v.Permissions = perms.String()
} else if v.BlobName == "" {
// Make sure the permission characters are in the correct order
perms := &ContainerSASPermissions{}
if err := perms.Parse(v.Permissions); err != nil {
return SASQueryParameters{}, err
}
v.Permissions = perms.String()
} else {
resource = "b"
// Make sure the permission characters are in the correct order
perms := &BlobSASPermissions{}
if err := perms.Parse(v.Permissions); err != nil {
return SASQueryParameters{}, err
}
v.Permissions = perms.String()
}
if v.Version == "" {
v.Version = SASVersion
}
startTime, expiryTime, snapshotTime := FormatTimesForSASSigning(v.StartTime, v.ExpiryTime, v.SnapshotTime)

signedIdentifier := v.Identifier

udk := credential.getUDKParams()

if udk != nil {
udkStart, udkExpiry, _ := FormatTimesForSASSigning(*udk.SignedStart, *udk.SignedExpiry, time.Time{})
//I don't like this answer to combining the functions
//But because signedIdentifier and the user delegation key strings share a place, this is an _OK_ way to do it.
signedIdentifier = strings.Join([]string{
*udk.SignedOid,
*udk.SignedTid,
udkStart,
udkExpiry,
*udk.SignedService,
*udk.SignedVersion,
v.PreauthorizedAgentObjectId,
v.AgentObjectId,
v.CorrelationId,
}, "\n")
}

// String to sign: http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx
stringToSign := strings.Join([]string{
v.Permissions,
startTime,
expiryTime,
getCanonicalName(credential.AccountName(), v.ContainerName, v.BlobName, v.Directory),
signedIdentifier,
v.IPRange.String(),
string(v.Protocol),
v.Version,
resource,
snapshotTime, // signed timestamp
v.CacheControl, // rscc
v.ContentDisposition, // rscd
v.ContentEncoding, // rsce
v.ContentLanguage, // rscl
v.ContentType}, // rsct
"\n")

signature, err := credential.ComputeHMACSHA256(stringToSign)
if err != nil {
return SASQueryParameters{}, err
}

p := SASQueryParameters{
// Common SAS parameters
version: v.Version,
protocol: v.Protocol,
startTime: v.StartTime,
expiryTime: v.ExpiryTime,
permissions: v.Permissions,
ipRange: v.IPRange,

// Container/Blob-specific SAS parameters
resource: resource,
identifier: v.Identifier,
cacheControl: v.CacheControl,
contentDisposition: v.ContentDisposition,
contentEncoding: v.ContentEncoding,
contentLanguage: v.ContentLanguage,
contentType: v.ContentType,
snapshotTime: v.SnapshotTime,
signedDirectoryDepth: getDirectoryDepth(v.Directory),
preauthorizedAgentObjectId: v.PreauthorizedAgentObjectId,
agentObjectId: v.AgentObjectId,
correlationId: v.CorrelationId,
// Calculated SAS signature
signature: signature,
}

//User delegation SAS specific parameters
if udk != nil {
p.signedOid = *udk.SignedOid
p.signedTid = *udk.SignedTid
p.signedStart = *udk.SignedStart
p.signedExpiry = *udk.SignedExpiry
p.signedService = *udk.SignedService
p.signedVersion = *udk.SignedVersion
}

return p, nil
}

// getCanonicalName computes the canonical name for a container or blob resource for SAS signing.
func getCanonicalName(account string, containerName string, blobName string, directoryName string) string {
// Container: "/blob/account/containername"
Expand Down
15 changes: 15 additions & 0 deletions sdk/storage/azblob/zc_service_client.go
Expand Up @@ -86,6 +86,21 @@ func NewServiceClientFromConnectionString(connectionString string, options *Clie
return NewServiceClientWithSharedKey(endpoint, credential, options)
}

//NewServiceClientWithUserDelegationCredential obtains a UserDelegationKey object using the base ServiceURL object.
//OAuth is required for this call, as well as any role that can delegate access to the storage account.
func (s *ServiceClient) NewServiceClientWithUserDelegationCredential(ctx context.Context, info KeyInfo, timeout *int32, requestID *string) (UserDelegationCredential, error) {
options := serviceClientGetUserDelegationKeyOptions{
RequestID: requestID,
Timeout: timeout,
}
sc := newServiceClient(s.client.endpoint, s.client.pl)
udk, err := sc.GetUserDelegationKey(ctx, info, &options)
if err != nil {
return UserDelegationCredential{}, err
}
return *NewUserDelegationCredential(strings.Split(s.client.endpoint, ".")[0], udk.UserDelegationKey), nil
}

// NewContainerClient creates a new ContainerClient object by concatenating containerName to the end of
// ServiceClient's URL. The new ContainerClient uses the same request policy pipeline as the ServiceClient.
// To change the pipeline, create the ContainerClient and then call its WithPipeline method passing in the
Expand Down
44 changes: 44 additions & 0 deletions sdk/storage/azblob/zc_user_delegation_credential.go
@@ -0,0 +1,44 @@
//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package azblob

import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)

// NewUserDelegationCredential creates a new UserDelegationCredential using a Storage account's name and a user delegation key from it
func NewUserDelegationCredential(accountName string, key UserDelegationKey) *UserDelegationCredential {
return &UserDelegationCredential{
accountName: accountName,
accountKey: key,
}
}

type UserDelegationCredential struct {
accountName string
accountKey UserDelegationKey
}

// AccountName returns the Storage account's name
func (f *UserDelegationCredential) AccountName() string {
return f.accountName
}

// ComputeHMAC
func (f *UserDelegationCredential) ComputeHMACSHA256(message string) (base64String string, err error) {
bytes, _ := base64.StdEncoding.DecodeString(*f.accountKey.Value)
h := hmac.New(sha256.New, bytes)
_, err = h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil)), err
}

// Private method to return important parameters for NewSASQueryParameters
func (f *UserDelegationCredential) getUDKParams() *UserDelegationKey {
return &f.accountKey
}
42 changes: 42 additions & 0 deletions sdk/storage/azblob/zt_examples_test.go
Expand Up @@ -1845,6 +1845,48 @@ func ExampleBlobClient_Download() {
fmt.Printf("Wrote %d bytes.\n", written)
}

func ExampleUserDelegationCredential() {
// Create Managed Identity (OAuth) Credentials using Client ID
clientOptions := azcore.ClientOptions{}
optsClientID := azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions, ID: azidentity.ClientID("7cf7db0d-...")}
cred, err := azidentity.NewManagedIdentityCredential(&optsClientID)
if err != nil {
log.Fatal(err)
}
clientOptionsAzBlob := azblob.ClientOptions{} // Same as azcore.ClientOptions using azblob instead

svcClient, err := azblob.NewServiceClient("svcURL", cred, &clientOptionsAzBlob)

// Set current and past time
currentTime := time.Now().UTC().Add(-10 * time.Second)
pastTime := currentTime.Add(48 * time.Hour)
info := azblob.KeyInfo{
Start: to.Ptr(currentTime.UTC().Format(azblob.SASTimeFormat)),
Expiry: to.Ptr(pastTime.UTC().Format(azblob.SASTimeFormat)),
}

udkResp, err := svcClient.NewServiceClientWithUserDelegationCredential(context.Background(), info, nil, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("User Delegation Key has been created for ", udkResp.AccountName())

// Create Managed Identity (OAuth) Credentials using Resource ID
optsResourceID := azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions, ID: azidentity.ResourceID("/subscriptions/...")}
cred, err = azidentity.NewManagedIdentityCredential(&optsResourceID)
if err != nil {
log.Fatal(err)
}

svcClient, err = azblob.NewServiceClient("svcURL", cred, &clientOptionsAzBlob)

udkResp, err = svcClient.NewServiceClientWithUserDelegationCredential(context.Background(), info, nil, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("User Delegation Key has been created for ", udkResp.AccountName())
}

//func ExampleUploadStreamToBlockBlob() {
// // From the Azure portal, get your Storage account blob service URL endpoint.
// accountName, accountKey := os.Getenv("AZURE_STORAGE_ACCOUNT_NAME"), os.Getenv("AZURE_STORAGE_ACCOUNT_KEY")
Expand Down
60 changes: 60 additions & 0 deletions sdk/storage/azblob/zt_user_delegation_test.go
@@ -0,0 +1,60 @@
//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package azblob

// TODO: Add tests for UserDelegationCredential
/*func (s *azblobUnrecordedTestSuite) TestUserDelegationCredential() {
_require := require.New(s.T())
testName := s.T().Name()

clientOptions := azcore.ClientOptions{}
opts1 := azidentity.ManagedIdentityCredentialOptions{ClientOptions: clientOptions, ID: nil}
cred, err := azidentity.NewManagedIdentityCredential(&opts1)
if err != nil {
log.Fatal(err)
}
_require.Nil(err)

svcClient, err := getServiceClient(nil, testAccountDefault, nil)
if err != nil {
s.Fail("Unable to fetch service client because " + err.Error())
}

// Set current and past time
currentTime := time.Now().UTC().Add(-10 * time.Second).Format(SASTimeFormat)
pastTime := time.Now().UTC().Add(-10 * time.Second).Add(48 * time.Hour).Format(SASTimeFormat)
info := KeyInfo{
Start: &currentTime,
Expiry: &pastTime,
}

ctx := context.Background()
udkResp, err := svcClient.NewServiceClientWithUserDelegationCredential(ctx, info, nil, nil)
if err != nil {
s.Fail("Unable to create user delegation credential because " + err.Error())
}

containerClient := createNewContainer(_require, generateContainerName(testName), svcClient)
defer deleteContainer(_require, containerClient)
src, err := containerClient.NewBlockBlobClient("src")
if err != nil {
s.Fail("Unable to fetch block blob client because " + err.Error())
}

// Get source blob url with OAuth Cred
srcBlobParts, _ := NewBlobURLParts(src.URL())
srcBlobParts.SAS, err = BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
ExpiryTime: time.Now().UTC().Add(1 * time.Hour),
ContainerName: srcBlobParts.ContainerName,
BlobName: srcBlobParts.BlobName,
Permissions: BlobSASPermissions{Read: true}.String(),
}.NewSASQueryParametersWithUserDelegation(&udkResp)
if err != nil {
s.T().Fatal(err)
}
}*/