From 27a80146d8b199d169d63be40a4f86779570b9bb Mon Sep 17 00:00:00 2001 From: siminsavani-msft <77068571+siminsavani-msft@users.noreply.github.com> Date: Tue, 23 Aug 2022 18:17:42 -0400 Subject: [PATCH] Adding user delegation credential --- sdk/storage/azblob/zc_sas_service.go | 137 ++++++++++++++++++ sdk/storage/azblob/zc_service_client.go | 15 ++ .../azblob/zc_user_delegation_credential.go | 44 ++++++ sdk/storage/azblob/zt_examples_test.go | 42 ++++++ sdk/storage/azblob/zt_user_delegation_test.go | 60 ++++++++ 5 files changed, 298 insertions(+) create mode 100644 sdk/storage/azblob/zc_user_delegation_credential.go create mode 100644 sdk/storage/azblob/zt_user_delegation_test.go diff --git a/sdk/storage/azblob/zc_sas_service.go b/sdk/storage/azblob/zc_sas_service.go index 488baed8c0c3..223a504cbfd8 100644 --- a/sdk/storage/azblob/zc_sas_service.go +++ b/sdk/storage/azblob/zc_sas_service.go @@ -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" diff --git a/sdk/storage/azblob/zc_service_client.go b/sdk/storage/azblob/zc_service_client.go index e75dd10b31e7..fd429a10c81c 100644 --- a/sdk/storage/azblob/zc_service_client.go +++ b/sdk/storage/azblob/zc_service_client.go @@ -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 diff --git a/sdk/storage/azblob/zc_user_delegation_credential.go b/sdk/storage/azblob/zc_user_delegation_credential.go new file mode 100644 index 000000000000..82657049ed8c --- /dev/null +++ b/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 +} diff --git a/sdk/storage/azblob/zt_examples_test.go b/sdk/storage/azblob/zt_examples_test.go index 3d8808dc8401..6c282ba417bd 100644 --- a/sdk/storage/azblob/zt_examples_test.go +++ b/sdk/storage/azblob/zt_examples_test.go @@ -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") diff --git a/sdk/storage/azblob/zt_user_delegation_test.go b/sdk/storage/azblob/zt_user_delegation_test.go new file mode 100644 index 000000000000..95723d71eac9 --- /dev/null +++ b/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: ¤tTime, + 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) + } +}*/