Supporting User Delegation SAS (#259)

* Changes to support new user delegation SAS

* Added support for user delegation SAS

* Support generation of Directory SAS

* Correcting UDK struct and updating SAS & Blob SAS query params

* Pinning down old SASVersion

* Renaming for SDK consistency, adding tests

* Reorganizing for CI

* Cleaning up tests

* Cleaning up tests due to CI

* Adding change to resolve CI issues

* Addressing PR comments

* Test for sdd

* Update sdd test

* Fix tests

Co-authored-by: Narasimha Kulkarni <63087328+nakulkar-msft@users.noreply.github.com>
Co-authored-by: Ze Qian Zhang <zezha@microsoft.com>
This commit is contained in:
siminsavani-msft 2021-08-27 11:20:32 -04:00 коммит произвёл GitHub
Родитель ce5190c9fe
Коммит deb21f705e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 325 добавлений и 96 удалений

Просмотреть файл

@ -10,21 +10,33 @@ import (
// BlobSASSignatureValues is used to generate a Shared Access Signature (SAS) for an Azure Storage container or blob.
// For more information, see https://docs.microsoft.com/rest/api/storageservices/constructing-a-service-sas
type BlobSASSignatureValues struct {
Version string `param:"sv"` // If not specified, this defaults to SASVersion
Protocol SASProtocol `param:"spr"` // See the SASProtocol* constants
StartTime time.Time `param:"st"` // Not specified if IsZero
ExpiryTime time.Time `param:"se"` // Not specified if IsZero
SnapshotTime time.Time
Permissions string `param:"sp"` // Create by initializing a ContainerSASPermissions or BlobSASPermissions and then call String()
IPRange IPRange `param:"sip"`
Identifier string `param:"si"`
ContainerName string
BlobName string // Use "" to create a Container SAS
CacheControl string // rscc
ContentDisposition string // rscd
ContentEncoding string // rsce
ContentLanguage string // rscl
ContentType string // rsct
Version string `param:"sv"` // If not specified, this defaults to SASVersion
Protocol SASProtocol `param:"spr"` // See the SASProtocol* constants
StartTime time.Time `param:"st"` // Not specified if IsZero
ExpiryTime time.Time `param:"se"` // Not specified if IsZero
SnapshotTime time.Time
Permissions string `param:"sp"` // Create by initializing a ContainerSASPermissions or BlobSASPermissions and then call String()
IPRange IPRange `param:"sip"`
Identifier string `param:"si"`
ContainerName string
BlobName string // Use "" to create a Container SAS
Directory string // Not nil for a directory SAS (ie sr=d)
CacheControl string // rscc
ContentDisposition string // rscd
ContentEncoding string // rsce
ContentLanguage string // rscl
ContentType string // rsct
BlobVersion string // sr=bv
PreauthorizedAgentObjectId string
AgentObjectId string
CorrelationId string
}
func getDirectoryDepth(path string) string {
if path == "" {
return ""
}
return fmt.Sprint(strings.Count(path, "/") + 1)
}
// NewSASQueryParameters uses an account's StorageAccountCredential to sign this signature values to produce
@ -44,7 +56,7 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(credential StorageAccountC
return SASQueryParameters{}, err
}
v.Permissions = perms.String()
} else if v.Version != "" {
} else if v.BlobVersion != "" {
resource = "bv"
//Make sure the permission characters are in the correct order
perms := &BlobSASPermissions{}
@ -52,6 +64,14 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(credential StorageAccountC
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{}
@ -88,6 +108,9 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(credential StorageAccountC
udkExpiry,
udk.SignedService,
udk.SignedVersion,
v.PreauthorizedAgentObjectId,
v.AgentObjectId,
v.CorrelationId,
}, "\n")
}
@ -96,7 +119,7 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(credential StorageAccountC
v.Permissions,
startTime,
expiryTime,
getCanonicalName(credential.AccountName(), v.ContainerName, v.BlobName),
getCanonicalName(credential.AccountName(), v.ContainerName, v.BlobName, v.Directory),
signedIdentifier,
v.IPRange.String(),
string(v.Protocol),
@ -123,15 +146,18 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(credential StorageAccountC
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,
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,
}
@ -150,12 +176,14 @@ func (v BlobSASSignatureValues) NewSASQueryParameters(credential StorageAccountC
}
// getCanonicalName computes the canonical name for a container or blob resource for SAS signing.
func getCanonicalName(account string, containerName string, blobName string) string {
func getCanonicalName(account string, containerName string, blobName string, directoryName string) string {
// Container: "/blob/account/containername"
// Blob: "/blob/account/containername/blobname"
elements := []string{"/blob/", account, "/", containerName}
if blobName != "" {
elements = append(elements, "/", strings.Replace(blobName, "\\", "/", -1))
} else if directoryName != "" {
elements = append(elements, "/", directoryName)
}
return strings.Join(elements, "")
}
@ -244,9 +272,8 @@ func (p *ContainerSASPermissions) Parse(s string) error {
// The BlobSASPermissions type simplifies creating the permissions string for an Azure Storage blob SAS.
// Initialize an instance of this type and then call its String method to set BlobSASSignatureValues's Permissions field.
type BlobSASPermissions struct{
Read, Add, Create, Write, Delete, DeletePreviousVersion, Tag bool
Execute, ModifyOwnership, ModifyPermissions bool // Hierarchical Namespace only
type BlobSASPermissions struct {
Read, Add, Create, Write, Delete, DeletePreviousVersion, Tag, List, Move, Execute, Ownership, Permissions bool
}
// String produces the SAS permissions string for an Azure Storage blob.
@ -274,13 +301,19 @@ func (p BlobSASPermissions) String() string {
if p.Tag {
b.WriteRune('t')
}
if p.List {
b.WriteRune('l')
}
if p.Move {
b.WriteRune('m')
}
if p.Execute {
b.WriteRune('e')
}
if p.ModifyOwnership {
if p.Ownership {
b.WriteRune('o')
}
if p.ModifyPermissions {
if p.Permissions {
b.WriteRune('p')
}
return b.String()
@ -305,12 +338,16 @@ func (p *BlobSASPermissions) Parse(s string) error {
p.DeletePreviousVersion = true
case 't':
p.Tag = true
case 'l':
p.List = true
case 'm':
p.Move = true
case 'e':
p.Execute = true
case 'o':
p.ModifyOwnership = true
p.Ownership = true
case 'p':
p.ModifyPermissions = true
p.Permissions = true
default:
return fmt.Errorf("invalid permission: '%v'", r)
}

Просмотреть файл

@ -83,37 +83,48 @@ func parseSASTimeString(val string) (t time.Time, timeFormat string, err error)
// This type defines the components used by all Azure Storage resources (Containers, Blobs, Files, & Queues).
type SASQueryParameters struct {
// All members are immutable or values so copies of this struct are goroutine-safe.
version string `param:"sv"`
services string `param:"ss"`
resourceTypes string `param:"srt"`
protocol SASProtocol `param:"spr"`
startTime time.Time `param:"st"`
expiryTime time.Time `param:"se"`
snapshotTime time.Time `param:"snapshot"`
ipRange IPRange `param:"sip"`
identifier string `param:"si"`
resource string `param:"sr"`
permissions string `param:"sp"`
signature string `param:"sig"`
cacheControl string `param:"rscc"`
contentDisposition string `param:"rscd"`
contentEncoding string `param:"rsce"`
contentLanguage string `param:"rscl"`
contentType string `param:"rsct"`
signedOid string `param:"skoid"`
signedTid string `param:"sktid"`
signedStart time.Time `param:"skt"`
signedExpiry time.Time `param:"ske"`
signedService string `param:"sks"`
signedVersion string `param:"skv"`
version string `param:"sv"`
services string `param:"ss"`
resourceTypes string `param:"srt"`
protocol SASProtocol `param:"spr"`
startTime time.Time `param:"st"`
expiryTime time.Time `param:"se"`
snapshotTime time.Time `param:"snapshot"`
ipRange IPRange `param:"sip"`
identifier string `param:"si"`
resource string `param:"sr"`
permissions string `param:"sp"`
signature string `param:"sig"`
cacheControl string `param:"rscc"`
contentDisposition string `param:"rscd"`
contentEncoding string `param:"rsce"`
contentLanguage string `param:"rscl"`
contentType string `param:"rsct"`
signedOid string `param:"skoid"`
signedTid string `param:"sktid"`
signedStart time.Time `param:"skt"`
signedService string `param:"sks"`
signedExpiry time.Time `param:"ske"`
signedVersion string `param:"skv"`
signedDirectoryDepth string `param:"sdd"`
preauthorizedAgentObjectId string `param:"saoid"`
agentObjectId string `param:"suoid"`
correlationId string `param:"scid"`
// private member used for startTime and expiryTime formatting.
stTimeFormat string
seTimeFormat string
}
func (p *SASQueryParameters) SignedOid() string {
return p.signedOid
func (p *SASQueryParameters) PreauthorizedAgentObjectId() string {
return p.preauthorizedAgentObjectId
}
func (p *SASQueryParameters) AgentObjectId() string {
return p.agentObjectId
}
func (p *SASQueryParameters) SignedCorrelationId() string {
return p.correlationId
}
func (p *SASQueryParameters) SignedTid() string {
@ -199,6 +210,10 @@ func (p *SASQueryParameters) ContentType() string {
return p.contentType
}
func (p *SASQueryParameters) SignedDirectoryDepth() string {
return p.signedDirectoryDepth
}
// IPRange represents a SAS IP range's start IP and (optionally) end IP.
type IPRange struct {
Start net.IP // Not specified if length = 0
@ -279,6 +294,14 @@ func newSASQueryParameters(values url.Values, deleteSASParametersFromValues bool
p.signedService = val
case "skv":
p.signedVersion = val
case "sdd":
p.signedDirectoryDepth = val
case "saoid":
p.preauthorizedAgentObjectId = val
case "suoid":
p.agentObjectId = val
case "scid":
p.correlationId = val
default:
isSASKey = false // We didn't recognize the query parameter
}
@ -347,6 +370,18 @@ func (p *SASQueryParameters) addToValues(v url.Values) url.Values {
if p.contentType != "" {
v.Add("rsct", p.contentType)
}
if p.signedDirectoryDepth != "" {
v.Add("sdd", p.signedDirectoryDepth)
}
if p.preauthorizedAgentObjectId != "" {
v.Add("saoid", p.preauthorizedAgentObjectId)
}
if p.agentObjectId != "" {
v.Add("suoid", p.agentObjectId)
}
if p.correlationId != "" {
v.Add("scid", p.correlationId)
}
return v
}

Просмотреть файл

@ -1,32 +1,108 @@
package azblob
// TODO: This test will be addressed, it is failing due to a service change
//Creates a container and tests permissions by listing blobs
/*func (s *aztestsSuite) TestUserDelegationSASContainer(c *chk.C) {
import (
"bytes"
"github.com/Azure/azure-pipeline-go/pipeline"
chk "gopkg.in/check.v1"
"net/url"
"strings"
"time"
)
func CreateUserDelegationKey(c *chk.C) (containerURL ContainerURL, containerName string, blobURL BlockBlobURL, blobName string, budk UserDelegationCredential, currentTime time.Time, p pipeline.Pipeline) {
// Accumulate prerequisite details to create storage etc.
bsu := getBSU()
containerURL, containerName := getContainerURL(c, bsu)
currentTime := time.Now().UTC()
containerURL, containerName = getContainerURL(c, bsu)
blobURL, blobName = getBlockBlobURL(c, containerURL)
currentTime = time.Now().UTC().Add(-10 * time.Second)
ocred, err := getOAuthCredential("")
if err != nil {
c.Fatal(err)
}
// Create pipeline w/ OAuth to handle user delegation key obtaining
p := NewPipeline(*ocred, PipelineOptions{})
// Create pipeline to handle requests
p = NewPipeline(*ocred, PipelineOptions{})
// Prepare user delegation key
bsu = bsu.WithPipeline(p)
keyInfo := NewKeyInfo(currentTime, currentTime.Add(48*time.Hour))
cudk, err := bsu.GetUserDelegationCredential(ctx, keyInfo, nil, nil)
budk, err = bsu.GetUserDelegationCredential(ctx, keyInfo, nil, nil) //MUST have TokenCredential
if err != nil {
c.Fatal(err)
}
return containerURL, containerName, blobURL, blobName, budk, currentTime, p
}
// Attempting to create User Delegation Key SAS with Incorrect Permissions, should return err
func (s *aztestsSuite) TestUserDelegationSASIncorrectPermissions(c *chk.C) {
_, containerName, _, blobName, cudk, currentTime, _ := CreateUserDelegationKey(c)
// Prepare User Delegation SAS query for Container
_, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "rdq",
ContainerName: containerName,
}.NewSASQueryParameters(cudk)
c.Assert(err, chk.NotNil)
// Prepare User Delegation SAS query for Blob; returns err due to wrong permission
_, err = BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "rdq",
ContainerName: containerName,
BlobName: blobName,
}.NewSASQueryParameters(cudk)
c.Assert(err, chk.NotNil)
}
// Creates a container with no permissions, upload fails due to lack of permissions
func (s *aztestsSuite) TestUserDelegationSASContainerNoPermissions(c *chk.C) {
containerURL, containerName, _, _, cudk, currentTime, p := CreateUserDelegationKey(c)
// Prepare User Delegation SAS query
cSAS, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "racwdl",
ContainerName: containerName,
}.NewSASQueryParameters(cudk)
if err != nil {
c.Fatal(err)
}
// Create anonymous pipeline
p = NewPipeline(NewAnonymousCredential(), PipelineOptions{})
// Create the container
_, err = containerURL.Create(ctx, Metadata{}, PublicAccessNone)
defer containerURL.Delete(ctx, ContainerAccessConditions{})
if err != nil {
c.Fatal(err)
}
// Craft a container URL w/ container UDK SAS
cURL := containerURL.URL()
cURL.RawQuery += cSAS.Encode()
cSASURL := NewContainerURL(cURL, p)
// Create blob; upload returns err due to lack of permissions
bblob := cSASURL.NewBlockBlobURL("test")
_, err = bblob.Upload(ctx, strings.NewReader("hello world!"), BlobHTTPHeaders{}, Metadata{}, BlobAccessConditions{}, DefaultAccessTier, nil, ClientProvidedKeyOptions{})
c.Assert(err, chk.NotNil)
}
// Creates a container with all permissions
func (s *aztestsSuite) TestUserDelegationSASContainerAllPermissions(c *chk.C) {
containerURL, containerName, _, _, cudk, currentTime, p := CreateUserDelegationKey(c)
// Prepare User Delegation SAS query
cSAS, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "racwdxlt",
ContainerName: containerName,
}.NewSASQueryParameters(cudk)
if err != nil {
@ -74,38 +150,18 @@ package azblob
if err != nil {
c.Fatal(err)
}
}*/
}
// TODO: This test will be addressed, it is failing due to a service change
// Creates a blob, takes a snapshot, downloads from snapshot, and deletes from the snapshot w/ the token
/*func (s *aztestsSuite) TestUserDelegationSASBlob(c *chk.C) {
// Accumulate prerequisite details to create storage etc.
bsu := getBSU()
containerURL, containerName := getContainerURL(c, bsu)
blobURL, blobName := getBlockBlobURL(c, containerURL)
currentTime := time.Now().UTC()
ocred, err := getOAuthCredential("")
if err != nil {
c.Fatal(err)
}
// Create pipeline to handle requests
p := NewPipeline(*ocred, PipelineOptions{})
// Prepare user delegation key
bsu = bsu.WithPipeline(p)
keyInfo := NewKeyInfo(currentTime, currentTime.Add(48*time.Hour))
budk, err := bsu.GetUserDelegationCredential(ctx, keyInfo, nil, nil) //MUST have TokenCredential
if err != nil {
c.Fatal(err)
}
// Creates a blob with all permissions, takes a snapshot, downloads from snapshot, and deletes from the snapshot w/ the token
func (s *aztestsSuite) TestUserDelegationSASBlobAllPermissions(c *chk.C) {
containerURL, containerName, blobURL, blobName, budk, currentTime, p := CreateUserDelegationKey(c)
// Prepare User Delegation SAS query
bSAS, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "rd",
Permissions: "racwdxtmeop",
ContainerName: containerName,
BlobName: blobName,
}.NewSASQueryParameters(budk)
@ -155,4 +211,105 @@ package azblob
if err != nil {
c.Fatal(err)
}
}*/
}
// Creates User Delegation SAS with saoid and checks if URL is correctly formed
func (s *aztestsSuite) TestUserDelegationSaoid(c *chk.C) {
_, containerName, blobURL, blobName, budk, currentTime, p := CreateUserDelegationKey(c)
saoid := newUUID().String()
// Prepare User Delegation SAS query
bSAS, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "rd",
ContainerName: containerName,
BlobName: blobName,
PreauthorizedAgentObjectId: saoid,
}.NewSASQueryParameters(budk)
if err != nil {
c.Fatal(err)
}
// Create pipeline
p = NewPipeline(NewAnonymousCredential(), PipelineOptions{})
// Append User Delegation SAS token to URL
bSASParts := NewBlobURLParts(blobURL.URL())
bSASParts.SAS = bSAS
bSASURL := NewBlockBlobURL(bSASParts.URL(), p)
c.Assert(strings.Contains(bSASURL.blobClient.url.RawQuery, "saoid="+saoid), chk.Equals, true)
}
// Creates User Delegation SAS with suoid and checks if URL is correctly formed
func (s *aztestsSuite) TestUserDelegationSuoid(c *chk.C) {
_, containerName, blobURL, blobName, budk, currentTime, p := CreateUserDelegationKey(c)
suoid := newUUID().String()
// Prepare User Delegation SAS query
bSAS, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "rd",
ContainerName: containerName,
BlobName: blobName,
AgentObjectId: suoid,
}.NewSASQueryParameters(budk)
if err != nil {
c.Fatal(err)
}
// Create pipeline
p = NewPipeline(NewAnonymousCredential(), PipelineOptions{})
// Append User Delegation SAS token to URL
bSASParts := NewBlobURLParts(blobURL.URL())
bSASParts.SAS = bSAS
bSASURL := NewBlockBlobURL(bSASParts.URL(), p)
c.Assert(strings.Contains(bSASURL.blobClient.url.RawQuery, "suoid="+suoid), chk.Equals, true)
}
// Creates User Delegation SAS with correlation id and checks if URL is correctly formed
func (s *aztestsSuite) TestUserDelegationCid(c *chk.C) {
_, containerName, blobURL, blobName, budk, currentTime, p := CreateUserDelegationKey(c)
cid := newUUID().String()
// Prepare User Delegation SAS query
bSAS, err := BlobSASSignatureValues{
Protocol: SASProtocolHTTPS,
StartTime: currentTime,
ExpiryTime: currentTime.Add(24 * time.Hour),
Permissions: "rd",
ContainerName: containerName,
BlobName: blobName,
CorrelationId: cid,
}.NewSASQueryParameters(budk)
if err != nil {
c.Fatal(err)
}
// Create pipeline
p = NewPipeline(NewAnonymousCredential(), PipelineOptions{})
// Append User Delegation SAS token to URL
bSASParts := NewBlobURLParts(blobURL.URL())
bSASParts.SAS = bSAS
bSASURL := NewBlockBlobURL(bSASParts.URL(), p)
c.Assert(strings.Contains(bSASURL.blobClient.url.RawQuery, "cid="+cid), chk.Equals, true)
}
func (s *aztestsSuite) TestParseSASQueryParams(c *chk.C) {
const blobURL = "https://myaccount.blob.core.windows.net/mycontainer/testfile?sp=r&st=2021-03-19T03:48:02Z&se=2021-03-19T11:48:02Z&spr=https&sv=2020-02-10&sr=d&sdd=10&sig=invalidsignature"
testURL, _ := url.Parse(blobURL)
bURLParts := NewBlobURLParts(*testURL)
sas := bURLParts.SAS
c.Assert(sas, chk.NotNil)
c.Assert(sas.resource, chk.Equals, "d")
c.Assert(sas.SignedDirectoryDepth(), chk.Equals, "10")
c.Assert(sas.protocol, chk.Equals, SASProtocolHTTPS)
}

Просмотреть файл

@ -7777,7 +7777,7 @@ func init() {
}
const (
rfc3339Format = "2006-01-02T15:04:05.0000000Z07:00"
rfc3339Format = "2006-01-02T15:04:05Z"
)
// used to convert times from UTC to GMT before sending across the wire