Skip to content

Commit

Permalink
Adding UDK/UDC to service client (#19141)
Browse files Browse the repository at this point in the history
* Adding UDK/UDC to service client

* Adding GetUDKSASURL

* Removing changes as requested

* Additional changes to remove

* Addressing comments, cleaning up code, adding wip test

* Modifications to test

* Clean up

* Addressing comments in PR

* Adding helper methods to only allow SDK calls

* Adding to example_test and making udc immutable

* Adding helper method for GetAccountName

* Additional clean up

* Minor changes & commenting out test

* Renaming of opts

* Changing option name for GetUserDelegationCredential

* Minor type renaming

* Putting GetUserDelegationCredential on service.client

* Adding doc comment

* Updating doc comment for KeyInfo

* Fixing example test with handle error

* Fixing GetUserDelegationCredential and examples - Service.Client

* Removing NewUserDelegationCredential from models.go
  • Loading branch information
siminsavani-msft committed Sep 23, 2022
1 parent 425f9f6 commit 6ba57ce
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 40 deletions.
2 changes: 1 addition & 1 deletion sdk/storage/azblob/blob/client.go
Expand Up @@ -255,7 +255,7 @@ func (b *Client) GetSASURL(permissions sas.BlobPermissions, start time.Time, exp

StartTime: start.UTC(),
ExpiryTime: expiry.UTC(),
}.Sign(b.sharedKey())
}.SignWithSharedKey(b.sharedKey())

if err != nil {
return "", err
Expand Down
4 changes: 2 additions & 2 deletions sdk/storage/azblob/blob/client_test.go
Expand Up @@ -114,7 +114,7 @@ func (s *BlobUnrecordedTestsSuite) TestCreateBlobClientWithSnapshotAndSAS() {
Permissions: to.Ptr(sas.AccountPermissions{Read: true, List: true}).String(),
Services: to.Ptr(sas.AccountServices{Blob: true}).String(),
ResourceTypes: to.Ptr(sas.AccountResourceTypes{Container: true, Object: true}).String(),
}.Sign(credential)
}.SignWithSharedKey(credential)
_require.Nil(err)

parts, err := blob.ParseURL(bbClient.URL())
Expand Down Expand Up @@ -156,7 +156,7 @@ func (s *BlobUnrecordedTestsSuite) TestCreateBlobClientWithSnapshotAndSASUsingCo
Permissions: to.Ptr(sas.AccountPermissions{Read: true, List: true}).String(),
Services: to.Ptr(sas.AccountServices{Blob: true}).String(),
ResourceTypes: to.Ptr(sas.AccountResourceTypes{Container: true, Object: true}).String(),
}.Sign(credential)
}.SignWithSharedKey(credential)
_require.Nil(err)

parts, err := blob.ParseURL(bbClient.URL())
Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azblob/blockblob/client_test.go
Expand Up @@ -1169,7 +1169,7 @@ func (s *BlockBlobUnrecordedTestsSuite) TestSetTierOnCopyBlockBlobFromURL() {
Permissions: to.Ptr(sas.AccountPermissions{Read: true, List: true}).String(),
Services: to.Ptr(sas.AccountServices{Blob: true}).String(),
ResourceTypes: to.Ptr(sas.AccountResourceTypes{Container: true, Object: true}).String(),
}.Sign(credential)
}.SignWithSharedKey(credential)
_require.Nil(err)

srcBlobParts, _ := blob.ParseURL(srcBlob.URL())
Expand Down
2 changes: 1 addition & 1 deletion sdk/storage/azblob/container/client.go
Expand Up @@ -312,7 +312,7 @@ func (c *Client) GetSASURL(permissions sas.ContainerPermissions, start time.Time
Permissions: permissions.String(),
StartTime: start.UTC(),
ExpiryTime: expiry.UTC(),
}.Sign(c.sharedKey())
}.SignWithSharedKey(c.sharedKey())
if err != nil {
return "", err
}
Expand Down
64 changes: 64 additions & 0 deletions sdk/storage/azblob/internal/exported/user_delegation_credential.go
@@ -0,0 +1,64 @@
//go:build go1.18
// +build go1.18

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

package exported

import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/internal/generated"
)

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

// UserDelegationKey contains UserDelegationKey.
type UserDelegationKey = generated.UserDelegationKey

// UserDelegationCredential contains an account's name and its user delegation key.
type UserDelegationCredential struct {
accountName string
userDelegationKey UserDelegationKey
}

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

// GetUDKParams is a helper method for accessing the user delegation key parameters outside of this package.
func GetAccountName(udc *UserDelegationCredential) string {
return udc.getAccountName()
}

// computeHMACSHA256 generates a hash signature for an HTTP request or for a SAS.
func (f *UserDelegationCredential) computeHMACSHA256(message string) (string, error) {
bytes, _ := base64.StdEncoding.DecodeString(*f.userDelegationKey.Value)
h := hmac.New(sha256.New, bytes)
_, err := h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil)), err
}

// ComputeUDCHMACSHA256 is a helper method for computing the signed string outside of this package.
func ComputeUDCHMACSHA256(udc *UserDelegationCredential, message string) (string, error) {
return udc.computeHMACSHA256(message)
}

// GetUDKParams returns UserDelegationKey
func (f *UserDelegationCredential) getUDKParams() *UserDelegationKey {
return &f.userDelegationKey
}

// GetUDKParams is a helper method for accessing the user delegation key parameters outside of this package.
func GetUDKParams(udc *UserDelegationCredential) *UserDelegationKey {
return udc.getUDKParams()
}
72 changes: 70 additions & 2 deletions sdk/storage/azblob/sas/account.go
Expand Up @@ -19,6 +19,9 @@ import (
// SharedKeyCredential contains an account's name and its primary or secondary key.
type SharedKeyCredential = exported.SharedKeyCredential

// UserDelegationCredential contains an account's name and its user delegation key.
type UserDelegationCredential = exported.UserDelegationCredential

// AccountSignatureValues is used to generate a Shared Access Signature (SAS) for an Azure Storage account.
// For more information, see https://docs.microsoft.com/rest/api/storageservices/constructing-an-account-sas
type AccountSignatureValues struct {
Expand All @@ -32,9 +35,9 @@ type AccountSignatureValues struct {
ResourceTypes string `param:"srt"` // Create by initializing AccountSASResourceTypes and then call String()
}

// Sign uses an account's shared key credential to sign this signature values to produce
// SignWithSharedKey uses an account's shared key credential to sign this signature values to produce
// the proper SAS query parameters.
func (v AccountSignatureValues) Sign(sharedKeyCredential *SharedKeyCredential) (QueryParameters, error) {
func (v AccountSignatureValues) SignWithSharedKey(sharedKeyCredential *SharedKeyCredential) (QueryParameters, error) {
// https://docs.microsoft.com/en-us/rest/api/storageservices/Constructing-an-Account-SAS
if v.ExpiryTime.IsZero() || v.Permissions == "" || v.ResourceTypes == "" || v.Services == "" {
return QueryParameters{}, errors.New("account SAS is missing at least one of these: ExpiryTime, Permissions, Service, or ResourceType")
Expand Down Expand Up @@ -87,6 +90,71 @@ func (v AccountSignatureValues) Sign(sharedKeyCredential *SharedKeyCredential) (
return p, nil
}

// SignWithUserDelegation uses an account's UserDelegationKey to sign this signature values to produce the proper SAS query parameters.
func (v AccountSignatureValues) SignWithUserDelegation(userDelegationCredential *UserDelegationCredential) (QueryParameters, error) {
// https://docs.microsoft.com/en-us/rest/api/storageservices/Constructing-an-Account-SAS
if v.ExpiryTime.IsZero() || v.Permissions == "" || v.ResourceTypes == "" || v.Services == "" {
return QueryParameters{}, errors.New("account SAS is missing at least one of these: ExpiryTime, Permissions, Service, or ResourceType")
}
if v.Version == "" {
v.Version = Version
}

perms, err := parseAccountPermissions(v.Permissions)
if err != nil {
return QueryParameters{}, err
}
v.Permissions = perms.String()

startTime, expiryTime, _ := formatTimesForSigning(v.StartTime, v.ExpiryTime, time.Time{})

stringToSign := strings.Join([]string{
exported.GetAccountName(userDelegationCredential),
v.Permissions,
v.Services,
v.ResourceTypes,
startTime,
expiryTime,
v.IPRange.String(),
string(v.Protocol),
v.Version,
""}, // That is right, the account SAS requires a terminating extra newline
"\n")

signature, err := exported.ComputeUDCHMACSHA256(userDelegationCredential, stringToSign)
if err != nil {
return QueryParameters{}, err
}
p := QueryParameters{
// Common SAS parameters
version: v.Version,
protocol: v.Protocol,
startTime: v.StartTime,
expiryTime: v.ExpiryTime,
permissions: v.Permissions,
ipRange: v.IPRange,

// Account-specific SAS parameters
services: v.Services,
resourceTypes: v.ResourceTypes,

// Calculated SAS signature
signature: signature,
}

udk := exported.GetUDKParams(userDelegationCredential)

//User delegation SAS specific parameters
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
}

// AccountPermissions type simplifies creating the permissions string for an Azure Storage Account SAS.
// Initialize an instance of this type and then call its String method to set AccountSASSignatureValues's Permissions field.
type AccountPermissions struct {
Expand Down
142 changes: 111 additions & 31 deletions sdk/storage/azblob/sas/service.go
Expand Up @@ -47,9 +47,8 @@ func getDirectoryDepth(path string) string {
return fmt.Sprint(strings.Count(path, "/") + 1)
}

// Sign uses an account's StorageAccountCredential to sign this signature values to produce the proper SAS query parameters.
// See: StorageAccountCredential. Compatible with both UserDelegationCredential and SharedKeyCredential
func (v BlobSignatureValues) Sign(sharedKeyCredential *SharedKeyCredential) (QueryParameters, error) {
// SignWithSharedKey uses an account's SharedKeyCredential to sign this signature values to produce the proper SAS query parameters.
func (v BlobSignatureValues) SignWithSharedKey(sharedKeyCredential *SharedKeyCredential) (QueryParameters, error) {
if sharedKeyCredential == nil {
return QueryParameters{}, fmt.Errorf("cannot sign SAS query without Shared Key Credential")
}
Expand Down Expand Up @@ -82,25 +81,6 @@ func (v BlobSignatureValues) Sign(sharedKeyCredential *SharedKeyCredential) (Que

signedIdentifier := v.Identifier

//udk := sharedKeyCredential.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,
Expand Down Expand Up @@ -151,15 +131,115 @@ func (v BlobSignatureValues) Sign(sharedKeyCredential *SharedKeyCredential) (Que
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
}

// SignWithUserDelegation uses an account's UserDelegationCredential to sign this signature values to produce the proper SAS query parameters.
func (v BlobSignatureValues) SignWithUserDelegation(userDelegationCredential *UserDelegationCredential) (QueryParameters, error) {
if userDelegationCredential == nil {
return QueryParameters{}, fmt.Errorf("cannot sign SAS query without User Delegation Key")
}

//Make sure the permission characters are in the correct order
perms, err := parseBlobPermissions(v.Permissions)
if err != nil {
return QueryParameters{}, err
}
v.Permissions = perms.String()

resource := "c"
if !v.SnapshotTime.IsZero() {
resource = "bs"
} else if v.BlobVersion != "" {
resource = "bv"
} else if v.Directory != "" {
resource = "d"
v.BlobName = ""
} else if v.BlobName == "" {
// do nothing
} else {
resource = "b"
}

if v.Version == "" {
v.Version = Version
}
startTime, expiryTime, snapshotTime := formatTimesForSigning(v.StartTime, v.ExpiryTime, v.SnapshotTime)

udk := exported.GetUDKParams(userDelegationCredential)

udkStart, udkExpiry, _ := formatTimesForSigning(*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(exported.GetAccountName(userDelegationCredential), 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 := exported.ComputeUDCHMACSHA256(userDelegationCredential, stringToSign)
if err != nil {
return QueryParameters{}, err
}

p := QueryParameters{
// 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
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
}
Expand Down

0 comments on commit 6ba57ce

Please sign in to comment.