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

Containeracl #422

Merged
merged 34 commits into from
Nov 8, 2016
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0bfe6be
beginning of blob lease functionality
kpfaulkner Oct 6, 2016
daf1095
breaklease with and without break period working
kpfaulkner Oct 14, 2016
5161cd3
change lease working
kpfaulkner Oct 14, 2016
15e14b1
renew lease working
kpfaulkner Oct 14, 2016
4a35137
refactor common code into leaseCommonPut
kpfaulkner Oct 15, 2016
ab94ed9
all unit tests passing
kpfaulkner Oct 16, 2016
080d2ef
tweak of godoc
kpfaulkner Oct 16, 2016
db88e32
Set container ACL initial code
kpfaulkner Oct 18, 2016
473d57d
change so param is struct and can handle optional properties
kpfaulkner Oct 18, 2016
5b3af80
adding in SAS part of permissions
kpfaulkner Oct 18, 2016
63cb499
Generate XML payload for setting permissions
kpfaulkner Oct 18, 2016
309d477
permissions working unsure about access policy:
kpfaulkner Oct 19, 2016
f9a0fd1
working on XML deserialization, WIP
kpfaulkner Oct 20, 2016
4184e8d
GetContainerPermission returning permissions correctly. Check for ref…
kpfaulkner Oct 20, 2016
cce5bd0
merged master
kpfaulkner Oct 20, 2016
907bd6c
containerACL tests in progress
kpfaulkner Oct 20, 2016
6083a83
upstream merge
kpfaulkner Oct 20, 2016
ac366db
Merge branch 'master' into containeracl
kpfaulkner Oct 20, 2016
362ef1d
SetContainerPermission and GetContainerPermission tests
kpfaulkner Oct 21, 2016
5bf4861
removing some debugging
kpfaulkner Oct 21, 2016
831ca62
formatting fix causing lint error
kpfaulkner Oct 21, 2016
522dfdc
changes based on Mcardosos' feedback.
kpfaulkner Oct 26, 2016
f22ad3d
marshalling working
kpfaulkner Oct 28, 2016
bb22d80
fixing test
kpfaulkner Oct 28, 2016
21f854e
test tweak
kpfaulkner Oct 28, 2016
7287de8
debugging tests working fine locally
kpfaulkner Oct 28, 2016
84c0f6c
rounding of access policy times
kpfaulkner Oct 28, 2016
0df936c
force rounding of dates for policy
kpfaulkner Oct 28, 2016
24913ed
changes based on review
kpfaulkner Oct 31, 2016
6a9edd1
capturing potential error from closing body
kpfaulkner Nov 2, 2016
3218bf1
change accesspolicy to convert to UTC
kpfaulkner Nov 4, 2016
5b8d080
reverting to rounding to second.
kpfaulkner Nov 4, 2016
ded4c38
rounding times for tests so test reflects reality
kpfaulkner Nov 4, 2016
c47bff4
typo
kpfaulkner Nov 4, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 209 additions & 3 deletions storage/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -301,6 +302,65 @@ const (
ContainerAccessTypeContainer ContainerAccessType = "container"
)

// ContainerAccessOptions are used when setting ACLs of containers (after creation)
type ContainerAccessOptions struct {
ContainerAccess ContainerAccessType
Timeout int
LeaseID string
}

// AccessPolicyDetails are used for SETTING policies
type AccessPolicyDetails struct {
ID string
StartTime time.Time
ExpiryTime time.Time
CanRead bool
CanWrite bool
CanDelete bool
}

// ContainerPermissions is used when setting permissions and Access Policies for containers.
type ContainerPermissions struct {
AccessOptions ContainerAccessOptions
AccessPolicy AccessPolicyDetails
}

// AccessPolicyDetailsXML has specifics about an access policy
// annotated with XML details.
type AccessPolicyDetailsXML struct {
StartTime time.Time `xml:"Start"`
ExpiryTime time.Time `xml:"Expiry"`
Permission string `xml:"Permission"`
}

// SignedIdentifier is a wrapper for a specific policy
type SignedIdentifier struct {
ID string `xml:"Id"`
AccessPolicy AccessPolicyDetailsXML `xml:"AccessPolicy"`
}

// SignedIdentifiers part of the response from GetPermissions call.
type SignedIdentifiers struct {
SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"`
}

// AccessPolicy is the response type from the GetPermissions call.
type AccessPolicy struct {
SignedIdentifiersList SignedIdentifiers `xml:"SignedIdentifiers"`
}

// ContainerAccessResponse is returned for the GetContainerPermissions function.
// This contains both the permission and access policy for the container.
type ContainerAccessResponse struct {
ContainerAccess ContainerAccessType
AccessPolicy SignedIdentifiers
}

// ContainerAccessHeader references header used when setting/getting container ACL
const (
ContainerAccessHeader string = "x-ms-blob-public-access"
)

// Maximum sizes (per REST API) for various concepts
const (
MaxBlobBlockSize = 4 * 1024 * 1024
Expand Down Expand Up @@ -416,7 +476,7 @@ func (b BlobStorageClient) createContainer(name string, access ContainerAccessTy

headers := b.client.getStandardHeaders()
if access != "" {
headers["x-ms-blob-public-access"] = string(access)
headers[ContainerAccessHeader] = string(access)
}
return b.client.exec(verb, uri, headers, nil)
}
Expand All @@ -438,6 +498,96 @@ func (b BlobStorageClient) ContainerExists(name string) (bool, error) {
return false, err
}

// SetContainerPermissions sets up container permissions as per https://msdn.microsoft.com/en-us/library/azure/dd179391.aspx
func (b BlobStorageClient) SetContainerPermissions(container string, containerPermissions ContainerPermissions) error {
params := url.Values{
"restype": {"container"},
"comp": {"acl"},
}

if containerPermissions.AccessOptions.Timeout > 0 {
params.Add("timeout", strconv.Itoa(containerPermissions.AccessOptions.Timeout))
}

uri := b.client.getEndpoint(blobServiceName, pathForContainer(container), params)
headers := b.client.getStandardHeaders()
if containerPermissions.AccessOptions.ContainerAccess != "" {
headers[ContainerAccessHeader] = string(containerPermissions.AccessOptions.ContainerAccess)
}

if containerPermissions.AccessOptions.LeaseID != "" {
headers[leaseID] = containerPermissions.AccessOptions.LeaseID
}

// generate the XML for the SharedAccessSignature if required.
accessPolicyXML, err := generateAccessPolicy(containerPermissions.AccessPolicy)
if err != nil {
return err
}

var resp *storageResponse
if accessPolicyXML != "" {
headers["Content-Length"] = strconv.Itoa(len(accessPolicyXML))
resp, err = b.client.exec("PUT", uri, headers, strings.NewReader(accessPolicyXML))
} else {
resp, err = b.client.exec("PUT", uri, headers, nil)
}

if err != nil {
return err
}

if resp != nil {
defer resp.body.Close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to capture and return the error object returned from Close() if it's not nil. See PR #431 for an example (util.go).


if resp.statusCode != http.StatusOK {
return errors.New("Unable to set permissions")
}
}
return nil
}

// GetContainerPermissions gets the container permissions as per https://msdn.microsoft.com/en-us/library/azure/dd179469.aspx
// If timeout is 0 then it will not be passed to Azure
// leaseID will only be passed to Azure if populated
// Returns permissionResponse which is combined permissions and AccessPolicy
func (b BlobStorageClient) GetContainerPermissions(container string, timeout int, leaseID string) (permissionResponse *ContainerAccessResponse, err error) {
params := url.Values{"restype": {"container"},
"comp": {"acl"}}

if timeout > 0 {
params.Add("timeout", strconv.Itoa(timeout))
}

uri := b.client.getEndpoint(blobServiceName, pathForContainer(container), params)
headers := b.client.getStandardHeaders()

if leaseID != "" {
headers[leaseID] = leaseID
}

resp, err := b.client.exec("GET", uri, headers, nil)
if err != nil {
return nil, err
}

// containerAccess. Blob, Container, empty
containerAccess := resp.headers.Get(http.CanonicalHeaderKey(ContainerAccessHeader))

defer resp.body.Close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as my above comment.

var out AccessPolicy
err = xmlUnmarshal(resp.body, &out.SignedIdentifiersList)
if err != nil {
return nil, err
}

permissionResponse = &ContainerAccessResponse{}
permissionResponse.AccessPolicy = out.SignedIdentifiersList
permissionResponse.ContainerAccess = ContainerAccessType(containerAccess)

return permissionResponse, nil
}

// DeleteContainer deletes the container with given name on the storage
// account. If the container does not exist returns error.
//
Expand Down Expand Up @@ -614,8 +764,6 @@ func (b BlobStorageClient) AcquireLease(container string, name string, leaseTime
return returnedLeaseID, nil
}

// what should we return in case of HTTP 201 but no lease ID?
// or it just cant happen? (brave words)
return "", errors.New("LeaseID not returned")
}

Expand Down Expand Up @@ -1276,3 +1424,61 @@ func blobSASStringToSign(signedVersion, canonicalizedResource, signedExpiry, sig
}
return "", errors.New("storage: not implemented SAS for versions earlier than 2013-08-15")
}

func generatePermissions(accessPolicy AccessPolicyDetails) (permissions string) {
// generate the permissions string (rwd).
// still want the end user API to have bool flags.
permissions = ""

if accessPolicy.CanRead {
permissions += "r"
}

if accessPolicy.CanWrite {
permissions += "w"
}

if accessPolicy.CanDelete {
permissions += "d"
}

return permissions
}

// convertAccessPolicyToXMLStructs converts between AccessPolicyDetails which is a struct better for API usage to the
// AccessPolicy struct which will get converted to XML.
func convertAccessPolicyToXMLStructs(accessPolicy AccessPolicyDetails) SignedIdentifiers {
return SignedIdentifiers{
SignedIdentifiers: []SignedIdentifier{
{
ID: accessPolicy.ID,
AccessPolicy: AccessPolicyDetailsXML{
StartTime: accessPolicy.StartTime.Round(time.Second),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last comment. In think it would be better if .UTC() was included here, instead of it being used when defining StartTime and ExpiryTime. createContainerPermissions woundn't need .UTC(). Also, do not round to the second, but to time.Nanosecond * 100 so all 7 decimals Azure expects can be used :D

ExpiryTime: accessPolicy.ExpiryTime.Round(time.Second),
Permission: generatePermissions(accessPolicy),
},
},
},
}
}

// generateAccessPolicy generates the XML access policy used as the payload for SetContainerPermissions.
func generateAccessPolicy(accessPolicy AccessPolicyDetails) (accessPolicyXML string, err error) {

if accessPolicy.ID != "" {
signedIdentifiers := convertAccessPolicyToXMLStructs(accessPolicy)
body, _, err := xmlMarshal(signedIdentifiers)
if err != nil {
return "", err
}

xmlByteArray, err := ioutil.ReadAll(body)
if err != nil {
return "", err
}
accessPolicyXML = string(xmlByteArray)
return accessPolicyXML, nil
}

return "", nil
}
110 changes: 110 additions & 0 deletions storage/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,115 @@ func (s *StorageBlobSuite) TestSetBlobProperties(c *chk.C) {
c.Check(mPut.ContentLanguage, chk.Equals, props.ContentLanguage)
}

func (s *StorageBlobSuite) createContainerPermissions(accessType ContainerAccessType,
timeout int, leaseID string, ID string, canRead bool,
canWrite bool, canDelete bool) ContainerPermissions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a new line for readability

perms := ContainerPermissions{}
perms.AccessOptions.ContainerAccess = accessType
perms.AccessOptions.Timeout = timeout
perms.AccessOptions.LeaseID = leaseID

if ID != "" {
perms.AccessPolicy.ID = ID
perms.AccessPolicy.StartTime = time.Now().UTC().Round(time.Second)
perms.AccessPolicy.ExpiryTime = time.Now().UTC().Add(time.Hour * 10).Round(time.Second)
perms.AccessPolicy.CanRead = canRead
perms.AccessPolicy.CanWrite = canWrite
perms.AccessPolicy.CanDelete = canDelete
}

return perms
}

func (s *StorageBlobSuite) TestSetContainerPermissionsWithTimeoutSuccessfully(c *chk.C) {
cli := getBlobClient(c)
cnt := randContainer()

c.Assert(cli.CreateContainer(cnt, ContainerAccessTypePrivate), chk.IsNil)
defer cli.deleteContainer(cnt)

perms := s.createContainerPermissions(ContainerAccessTypeBlob, 30, "", "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTa=", true, true, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question, where does the ID come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just made it up (well stole from a previous test). Just needed to be a unique string... and that's pretty unique :)


err := cli.SetContainerPermissions(cnt, perms)
c.Assert(err, chk.IsNil)
}

func (s *StorageBlobSuite) TestSetContainerPermissionsSuccessfully(c *chk.C) {
cli := getBlobClient(c)
cnt := randContainer()

c.Assert(cli.CreateContainer(cnt, ContainerAccessTypePrivate), chk.IsNil)
defer cli.deleteContainer(cnt)

perms := s.createContainerPermissions(ContainerAccessTypeBlob, 0, "", "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTa=", true, true, true)

err := cli.SetContainerPermissions(cnt, perms)
c.Assert(err, chk.IsNil)
}

func (s *StorageBlobSuite) TestSetThenGetContainerPermissionsSuccessfully(c *chk.C) {
cli := getBlobClient(c)
cnt := randContainer()

c.Assert(cli.CreateContainer(cnt, ContainerAccessTypePrivate), chk.IsNil)
defer cli.deleteContainer(cnt)

perms := s.createContainerPermissions(ContainerAccessTypeBlob, 0, "", "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTa=", true, true, true)

err := cli.SetContainerPermissions(cnt, perms)
c.Assert(err, chk.IsNil)

returnedPerms, err := cli.GetContainerPermissions(cnt, 0, "")
c.Assert(err, chk.IsNil)

// check container permissions itself.
c.Assert(returnedPerms.ContainerAccess, chk.Equals, perms.AccessOptions.ContainerAccess)

// now check policy set.
c.Assert(returnedPerms.AccessPolicy.SignedIdentifiers, chk.HasLen, 1)
c.Assert(returnedPerms.AccessPolicy.SignedIdentifiers[0].ID, chk.Equals, perms.AccessPolicy.ID)

// test timestamps down the minutes
c.Assert(returnedPerms.AccessPolicy.SignedIdentifiers[0].AccessPolicy.StartTime.Format(time.RFC1123), chk.Equals, perms.AccessPolicy.StartTime.Format(time.RFC1123))
c.Assert(returnedPerms.AccessPolicy.SignedIdentifiers[0].AccessPolicy.ExpiryTime.Format(time.RFC1123), chk.Equals, perms.AccessPolicy.ExpiryTime.Format(time.RFC1123))
c.Assert(returnedPerms.AccessPolicy.SignedIdentifiers[0].AccessPolicy.Permission, chk.Equals, "rwd")
}

func (s *StorageBlobSuite) TestSetContainerPermissionsOnlySuccessfully(c *chk.C) {
cli := getBlobClient(c)
cnt := randContainer()

c.Assert(cli.CreateContainer(cnt, ContainerAccessTypePrivate), chk.IsNil)
defer cli.deleteContainer(cnt)

perms := s.createContainerPermissions(ContainerAccessTypeBlob, 0, "", "", true, true, true)

err := cli.SetContainerPermissions(cnt, perms)
c.Assert(err, chk.IsNil)
}

func (s *StorageBlobSuite) TestSetThenGetContainerPermissionsOnlySuccessfully(c *chk.C) {
cli := getBlobClient(c)
cnt := randContainer()

c.Assert(cli.CreateContainer(cnt, ContainerAccessTypePrivate), chk.IsNil)
defer cli.deleteContainer(cnt)

perms := s.createContainerPermissions(ContainerAccessTypeBlob, 0, "", "", true, true, true)

err := cli.SetContainerPermissions(cnt, perms)
c.Assert(err, chk.IsNil)

returnedPerms, err := cli.GetContainerPermissions(cnt, 0, "")
c.Assert(err, chk.IsNil)

// check container permissions itself.
c.Assert(returnedPerms.ContainerAccess, chk.Equals, perms.AccessOptions.ContainerAccess)

// now check there are NO policies set
c.Assert(returnedPerms.AccessPolicy.SignedIdentifiers, chk.HasLen, 0)
}

func (s *StorageBlobSuite) TestAcquireLeaseWithNoProposedLeaseID(c *chk.C) {
cli := getBlobClient(c)
cnt := randContainer()
Expand All @@ -690,6 +799,7 @@ func (s *StorageBlobSuite) TestAcquireLeaseWithNoProposedLeaseID(c *chk.C) {

_, err := cli.AcquireLease(cnt, blob, 30, "")
c.Assert(err, chk.NotNil)

}

func (s *StorageBlobSuite) TestAcquireLeaseWithProposedLeaseID(c *chk.C) {
Expand Down