From a8c839f9e33d60ec5a284f70964dcefeb7e0d8cf Mon Sep 17 00:00:00 2001 From: Sean Kane <68240067+seankane-msft@users.noreply.github.com> Date: Mon, 16 Aug 2021 12:22:49 -0400 Subject: [PATCH] [Tables] Add authentication via SAS credential (#15256) * changing recording to use test-proxy * updating client and test proxy * updated Transport Do to send actual request * whitespace * working implementation * updating internal recording * now I have a blank recording... * cleaning up * more clean up * transport Do is repeating, proxy is not parsing URL correctly * small changes * adding a policy to do the same thing * adding a second test for proxy and transport * further progress with Jeff, switching to only using a policy right now * cleaning up file * cleaning up * added more debugging, added two required headers that I was missing * proxy working on a single test, need to convert all tests to test proxy * cleaning up * moved over successfully :) * more helper functions * working for a subtest functionality * fixed up a service client test * converting more service tests * converted all service client tests * converted table client tests * fixed last two client tests * entity test * entity tests * converting access policy tests * all passing in record mode * batch tests * liveonly for batch tests * adding a sanitizer and more methods for recording * adding main method to start and stop the proxy automatically * adding test proxy step to pipeline * double dash on version arg * need a better way to get userful error reporting * explicitly running test-proxy in the background * adjust the proxy start and install to be compatible with specific process * dont run test proxy in background * didnt remove an import * header to proxy_test.go * fixes for pipeline * added loggin on accident * use nohup on linux machines * further simplify nohup usage * there are no exes on windows * fixing errors, passing in default env variable * force background for linux task. may need another iteration to place it within the quotes * fake aad credential * clean up after test-proxy * add print for record mode * another different attempt at starting the proxy * jobs apparently is an unrecognized command on our linux boxes * correct dumb typo in run tests * replace nuget install with docker * working fake credentials for client delete entity test * fixing fake credentials portion, passing locally * trying an insecureskipverify transport * debugging statements to find the cwd * removing blank recording * working implementation of sas * working sas credential for account signature * table sas is failing * leverage proxy tool, not docker * remove apostrophes * moving recordings one directory up * transitioning to using the sas written by storage team * using storage methods * copied storage code, sas needs to be appended in a different way * changing directory for test-proxy to start from * removing parenthesis * adding steps for validating ssl * switching to docker * adding start server script * working implementation if we can fix the prepending of 'recording' to the docker request finder * big refactor, moving stuff into recording file, cleaning up proxy_test.go file * last fixings * convert back to docker. windows and linux images now present * double condition * moving configuration to a separate file. preparing for eng/common move * tier 0 of trust. I don't expect this to work, but it's still worth a shot. maybe the devops agents do something differently * update condition to use variable syntax * guess I'm not using variables. removing * call start-server.ps1 * adjust build-test and configure-proxy to run the docker container in context * wrap in quotes for the container create. it's apparently a bad * adjust the initialize call * disable vet temporarily * looking for cert file in env variable PROXY_CERT * set proxy_cert environment variable to find that certificate * changing to crt file * use crt cert * want to see errors * powershell errors * correcting how the volume binds to the windows container * small modification for windows container * finally got the magic sauce * cleaning up to remove azcore from internal * removing recordings that use vcr * issue with the url creation * persist query params correctly in azcore.JoinPaths * removing print statements * return root * changing location of script * forgot the stop command * working table level sas implementation * adding start/end rk/pk functionality * removing print statements * removing more non-tables code * docstrings and removing storage only code * making sas tests live only * Fixing sas table name to be lowercase always, adding test to verify read only * Fixing sas table name to be lowercase always, adding test to verify read only * adding cosmos sas test * fix for params * apiview fixes * fixing test * undoing change * updates * error naming * fixed service client, was adding percallpolicies twice * simplification * aligning with main * converting from query pager to list pager for naming consistency * updating objects for consistency * jeffs feedback * small fixes * autorest.md changes * fixing tablename Co-authored-by: scbedd <45376673+scbedd@users.noreply.github.com> --- eng/pipelines/templates/steps/build-test.yml | 4 +- .../{start-server.ps1 => proxy-server.ps1} | 0 sdk/azcore/request.go | 36 ++- sdk/azcore/request_test.go | 2 +- sdk/tables/autorest.md | 11 +- sdk/tables/aztable/byte_array_response.go | 64 +++- .../aztable/cosmos_patch_transform_policy.go | 6 +- sdk/tables/aztable/entity.go | 39 +++ sdk/tables/aztable/errors.go | 4 + sdk/tables/aztable/go.mod | 1 + sdk/tables/aztable/models.go | 2 +- sdk/tables/aztable/proxy_test.go | 10 +- .../TestInsertEntity_cosmos.json} | 30 +- .../TestInsertEntity_storage.json} | 30 +- sdk/tables/aztable/sas_account.go | 189 +++++++++++ sdk/tables/aztable/sas_query_params.go | 299 ++++++++++++++++++ sdk/tables/aztable/sas_service.go | 156 +++++++++ .../aztable/shared_access_signature_test.go | 265 ++++++++++++++++ sdk/tables/aztable/table_batch_test.go | 10 +- sdk/tables/aztable/table_client.go | 29 +- sdk/tables/aztable/table_client_test.go | 10 +- sdk/tables/aztable/table_pagers.go | 22 +- sdk/tables/aztable/table_service_client.go | 51 ++- .../aztable/table_service_client_test.go | 7 +- .../aztable/table_transactional_batch.go | 8 +- sdk/tables/aztable/zc_table_constants.go | 1 - sdk/tables/aztable/zt_table_recorded_tests.go | 6 +- 27 files changed, 1174 insertions(+), 118 deletions(-) rename eng/scripts/{start-server.ps1 => proxy-server.ps1} (100%) rename sdk/tables/aztable/recordings/{TestUpsertEntity/TestUpsertEntity_cosmos.json => TestInsertEntity/TestInsertEntity_cosmos.json} (89%) rename sdk/tables/aztable/recordings/{TestUpsertEntity/TestUpsertEntity_storage.json => TestInsertEntity/TestInsertEntity_storage.json} (91%) create mode 100644 sdk/tables/aztable/sas_account.go create mode 100644 sdk/tables/aztable/sas_query_params.go create mode 100644 sdk/tables/aztable/sas_service.go create mode 100644 sdk/tables/aztable/shared_access_signature_test.go diff --git a/eng/pipelines/templates/steps/build-test.yml b/eng/pipelines/templates/steps/build-test.yml index 8adf13b2a8f3..669faba92a1b 100644 --- a/eng/pipelines/templates/steps/build-test.yml +++ b/eng/pipelines/templates/steps/build-test.yml @@ -37,7 +37,7 @@ steps: foreach ($td in $testDirs) { pushd $td - $(Build.SourcesDirectory)/eng/scripts/start-server.ps1 start + $(Build.SourcesDirectory)/eng/scripts/proxy-server.ps1 start Write-Host "##[command]Executing go test -run "^Test" -v -coverprofile coverage.txt $td | go-junit-report -set-exit-code > report.xml" go test -run "^Test" -v -coverprofile coverage.txt . > temp.txt @@ -49,7 +49,7 @@ steps: rm coverage.txt } - $(Build.SourcesDirectory)/eng/scripts/start-server.ps1 stop + $(Build.SourcesDirectory)/eng/scripts/proxy-server.ps1 stop } displayName: 'Run Tests' workingDirectory: '${{parameters.GoWorkspace}}' diff --git a/eng/scripts/start-server.ps1 b/eng/scripts/proxy-server.ps1 similarity index 100% rename from eng/scripts/start-server.ps1 rename to eng/scripts/proxy-server.ps1 diff --git a/sdk/azcore/request.go b/sdk/azcore/request.go index f7e1bb5af143..f74eed9e203c 100644 --- a/sdk/azcore/request.go +++ b/sdk/azcore/request.go @@ -67,23 +67,29 @@ func (ov opValues) get(value interface{}) bool { } // JoinPaths concatenates multiple URL path segments into one path, -// inserting path separation characters as required. -func JoinPaths(paths ...string) string { +// inserting path separation characters as required. JoinPaths will preserve +// query parameters in the root path +func JoinPaths(root string, paths ...string) string { if len(paths) == 0 { - return "" - } - path := paths[0] - for i := 1; i < len(paths); i++ { - if path[len(path)-1] == '/' && paths[i][0] == '/' { - // strip off trailing '/' to avoid doubling up - path = path[:len(path)-1] - } else if path[len(path)-1] != '/' && paths[i][0] != '/' { - // add a trailing '/' - path = path + "/" - } - path += paths[i] + return root + } + + qps := "" + if strings.Contains(root, "?") { + splitPath := strings.Split(root, "?") + root, qps = splitPath[0], splitPath[1] + } + + for i := 0; i < len(paths); i++ { + root = strings.TrimRight(root, "/") + paths[i] = strings.TrimLeft(paths[i], "/") + root += "/" + paths[i] + } + + if qps != "" { + return root + "?" + qps } - return path + return root } // NewRequest creates a new Request with the specified input. diff --git a/sdk/azcore/request_test.go b/sdk/azcore/request_test.go index 09ac6748f035..dfc0be2e095d 100644 --- a/sdk/azcore/request_test.go +++ b/sdk/azcore/request_test.go @@ -507,7 +507,7 @@ func TestNewRequestFail(t *testing.T) { } func TestJoinPaths(t *testing.T) { - if path := JoinPaths(); path != "" { + if path := JoinPaths(""); path != "" { t.Fatalf("unexpected path %s", path) } const expected = "http://test.contoso.com/path/one/path/two/path/three/path/four/" diff --git a/sdk/tables/autorest.md b/sdk/tables/autorest.md index ca2060b6b671..ed440312cd12 100644 --- a/sdk/tables/autorest.md +++ b/sdk/tables/autorest.md @@ -11,12 +11,15 @@ version: "^3.0.0" input-file: https://github.com/Azure/azure-rest-api-specs/blob/d744b6bcb95ab4034832ded556dbbe58f4287c5b/specification/cosmos-db/data-plane/Microsoft.Tables/preview/2019-02-02/table.json license-header: MICROSOFT_MIT_NO_VERSION clear-output-folder: false -output-folder: aztable -file-prefix: "zz_generated_" +output-folder: aztable/internal +# file-prefix: "zz_generated_" tag: package-2019-02 credential-scope: none -use: "@autorest/go@4.0.0-preview.23" -openapi-type: data-plane +use: "@autorest/go@4.0.0-preview.26" +# openapi-type: data-plane +module-version: 0.1.0 +modelerfour: + group-parameters: false ``` ### Go multi-api diff --git a/sdk/tables/aztable/byte_array_response.go b/sdk/tables/aztable/byte_array_response.go index cc0ee76a8916..eeb1eece86ae 100644 --- a/sdk/tables/aztable/byte_array_response.go +++ b/sdk/tables/aztable/byte_array_response.go @@ -9,7 +9,7 @@ import ( "time" ) -// ByteArrayResponse converts the MapOfInterfaceResponse.Value from a map[string]interface{} to a []byte +// ByteArrayResponse is the return type for a GetEntity operation. The entities properties are stored in the Value property type ByteArrayResponse struct { // ClientRequestID contains the information returned from the x-ms-client-request-id header response. ClientRequestID *string @@ -45,6 +45,7 @@ type ByteArrayResponse struct { XMSContinuationNextRowKey *string } +// newByteArrayResponse converts a MapofInterfaceResponse from a map[string]interface{} to a []byte. func newByteArrayResponse(m MapOfInterfaceResponse) (ByteArrayResponse, error) { marshalledValue, err := json.Marshal(m.Value) if err != nil { @@ -65,8 +66,8 @@ func newByteArrayResponse(m MapOfInterfaceResponse) (ByteArrayResponse, error) { }, nil } -// TableEntityQueryByteResponseResponse is the response envelope for operations that return a TableEntityQueryResponse type. -type TableEntityQueryByteResponseResponse struct { +// TableEntityListByteResponseResponse is the response envelope for operations that return a TableEntityQueryResponse type. +type TableEntityListByteResponseResponse struct { // ClientRequestID contains the information returned from the x-ms-client-request-id header response. ClientRequestID *string @@ -101,12 +102,12 @@ type TableEntityQueryByteResponse struct { Value [][]byte } -func castToByteResponse(resp *TableEntityQueryResponseResponse) (TableEntityQueryByteResponseResponse, error) { +func castToByteResponse(resp *TableEntityQueryResponseResponse) (TableEntityListByteResponseResponse, error) { marshalledValue := make([][]byte, 0) for _, e := range resp.TableEntityQueryResponse.Value { m, err := json.Marshal(e) if err != nil { - return TableEntityQueryByteResponseResponse{}, err + return TableEntityListByteResponseResponse{}, err } marshalledValue = append(marshalledValue, m) } @@ -116,7 +117,7 @@ func castToByteResponse(resp *TableEntityQueryResponseResponse) (TableEntityQuer Value: marshalledValue, } - return TableEntityQueryByteResponseResponse{ + return TableEntityListByteResponseResponse{ ClientRequestID: resp.ClientRequestID, Date: resp.Date, RawResponse: resp.RawResponse, @@ -127,3 +128,54 @@ func castToByteResponse(resp *TableEntityQueryResponseResponse) (TableEntityQuer XMSContinuationNextRowKey: resp.XMSContinuationNextRowKey, }, nil } + +type TableListResponse struct { + // The metadata response of the table. + OdataMetadata *string `json:"odata.metadata,omitempty"` + + // List of tables. + Value []*TableResponseProperties `json:"value,omitempty"` +} + +func tableListResponseFromQueryResponse(q *TableQueryResponse) *TableListResponse { + return &TableListResponse{ + OdataMetadata: q.OdataMetadata, + Value: q.Value, + } +} + +// TableListResponseResponse stores the results of a ListTables operation +type TableListResponseResponse struct { + // ClientRequestID contains the information returned from the x-ms-client-request-id header response. + ClientRequestID *string + + // Date contains the information returned from the Date header response. + Date *time.Time + + // RawResponse contains the underlying HTTP response. + RawResponse *http.Response + + // RequestID contains the information returned from the x-ms-request-id header response. + RequestID *string + + // The properties for the table query response. + TableListResponse *TableListResponse + + // Version contains the information returned from the x-ms-version header response. + Version *string + + // XMSContinuationNextTableName contains the information returned from the x-ms-continuation-NextTableName header response. + XMSContinuationNextTableName *string +} + +func listResponseFromQueryResponse(q TableQueryResponseResponse) *TableListResponseResponse { + return &TableListResponseResponse{ + ClientRequestID: q.ClientRequestID, + Date: q.Date, + RawResponse: q.RawResponse, + RequestID: q.RequestID, + TableListResponse: tableListResponseFromQueryResponse(q.TableQueryResponse), + Version: q.Version, + XMSContinuationNextTableName: q.XMSContinuationNextTableName, + } +} diff --git a/sdk/tables/aztable/cosmos_patch_transform_policy.go b/sdk/tables/aztable/cosmos_patch_transform_policy.go index b7b339221717..8403a0893e67 100644 --- a/sdk/tables/aztable/cosmos_patch_transform_policy.go +++ b/sdk/tables/aztable/cosmos_patch_transform_policy.go @@ -11,10 +11,10 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" ) -// CosmosPatchTransformPolicy transforms PATCH requests into POST requests with the "X-HTTP-Method":"MERGE" header set. -type CosmosPatchTransformPolicy struct{} +// cosmosPatchTransformPolicy transforms PATCH requests into POST requests with the "X-HTTP-Method":"MERGE" header set. +type cosmosPatchTransformPolicy struct{} -func (p CosmosPatchTransformPolicy) Do(req *azcore.Request) (*azcore.Response, error) { +func (p cosmosPatchTransformPolicy) Do(req *azcore.Request) (*azcore.Response, error) { transformPatchToCosmosPost(req) return req.Next() } diff --git a/sdk/tables/aztable/entity.go b/sdk/tables/aztable/entity.go index 509cb7fd5945..0836c87d08db 100644 --- a/sdk/tables/aztable/entity.go +++ b/sdk/tables/aztable/entity.go @@ -13,12 +13,43 @@ import ( // https://docs.microsoft.com/en-us/rest/api/storageservices/payload-format-for-table-service-operations +// The Entity type is the bare minimum properties for a valid Entity. These should be embedded in a custom struct +// type MyEntity struct { +// Entity +// Value int +// StringValue string +// BoolValue bool +// } +// myEntity := MyEntity{ +// Entity: Entity{ +// PartitionKey: "pk001", +// RowKey: "rk001", +// }, +// Value: 10, +// StringValue: "somestring", +// BoolValue: false, +// } type Entity struct { PartitionKey string RowKey string Timestamp EdmDateTime } +// EdmEntity is an entity that embeds the azcore.Entity type and has a Properties map for an unlimited +// number of custom properties. The EdmEntity will serialize EdmGuid/EdmInt64/EdmDateTime/EdmBinary according to Odata annotations +// myEntity := EdmEntity{ +// Entity: Entity{ +// PartitionKey: "pk001", +// RowKey: "rk001", +// } +// Properties: map[string]interface{}{ +// "Value": 10, +// "Binary": EdmBinary([]byte{"bytevalue"}), +// "DateTime": EdmDateTime(time.Now()), +// "Int64": EdmInt64(123456789012345), + +// } +// } type EdmEntity struct { Metadata string `json:"odata.metadata"` Id string `json:"odata.id"` @@ -127,6 +158,8 @@ func (e *EdmEntity) UnmarshalJSON(data []byte) (err error) { return } +// EdmBinary represents an Entity Property that is a byte slice. A byte slice wrapped in +// EdmBinary will also receive the correct odata annotation for round-trip accuracy. type EdmBinary []byte func (e EdmBinary) MarshalText() ([]byte, error) { @@ -142,6 +175,8 @@ func (e *EdmBinary) UnmarshalText(data []byte) error { return nil } +// EdmInt64 represents an entity property that is a 64-bit integer. Using EdmInt64 guarantees +// proper odata type annotations. type EdmInt64 int64 func (e EdmInt64) MarshalText() ([]byte, error) { @@ -157,6 +192,8 @@ func (e *EdmInt64) UnmarshalText(data []byte) error { return nil } +// EdmInt64 represents an entity property that is a GUID wrapped in a string. Using EdmGuid guarantees +// proper odata type annotations. type EdmGuid string func (e EdmGuid) MarshalText() ([]byte, error) { @@ -168,6 +205,8 @@ func (e *EdmGuid) UnmarshalText(data []byte) error { return nil } +// EdmDateTime represents an entity property that is a time.Time object. Using EdmDateTime guarantees +// proper odata type annotations. type EdmDateTime time.Time const rfc3339 = "2006-01-02T15:04:05.9999999Z" diff --git a/sdk/tables/aztable/errors.go b/sdk/tables/aztable/errors.go index 17aeabca6d8f..1152898a309e 100644 --- a/sdk/tables/aztable/errors.go +++ b/sdk/tables/aztable/errors.go @@ -3,6 +3,10 @@ package aztable +import "errors" + +var errInvalidUpdateMode = errors.New("invalid EntityUpdateMode") + func checkEntityForPkRk(entity *map[string]interface{}, err error) error { if _, ok := (*entity)[partitionKey]; !ok { return partitionKeyRowKeyError diff --git a/sdk/tables/aztable/go.mod b/sdk/tables/aztable/go.mod index 6c22932e4a4a..d221fb14275b 100644 --- a/sdk/tables/aztable/go.mod +++ b/sdk/tables/aztable/go.mod @@ -4,6 +4,7 @@ go 1.13 replace github.com/Azure/azure-sdk-for-go/sdk/internal => ../../internal replace github.com/Azure/azure-sdk-for-go/sdk/azidentity => ../../azidentity +replace github.com/Azure/azure-sdk-for-go/sdk/azcore => ../../azcore require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.16.2 diff --git a/sdk/tables/aztable/models.go b/sdk/tables/aztable/models.go index c6ec5fc9371c..476d07c102ca 100644 --- a/sdk/tables/aztable/models.go +++ b/sdk/tables/aztable/models.go @@ -3,7 +3,7 @@ package aztable -// QueryOptions contains a group of parameters for the Table.Query method. +// ListOptions contains a group of parameters for the Table.Query method. type ListOptions struct { // OData filter expression. Filter *string diff --git a/sdk/tables/aztable/proxy_test.go b/sdk/tables/aztable/proxy_test.go index 4a7f072d91c1..d79f395a4e11 100644 --- a/sdk/tables/aztable/proxy_test.go +++ b/sdk/tables/aztable/proxy_test.go @@ -91,10 +91,10 @@ func createTableServiceClientForRecording(t *testing.T, serviceURL string, cred func initClientTest(t *testing.T, service string, createTable bool) (*TableClient, func()) { var client *TableClient var err error - if service == string(StorageEndpoint) { + if service == string(storageEndpoint) { client, err = createStorageTableClient(t) require.NoError(t, err) - } else if service == string(CosmosEndpoint) { + } else if service == string(cosmosEndpoint) { client, err = createCosmosTableClient(t) require.NoError(t, err) } @@ -118,10 +118,10 @@ func initClientTest(t *testing.T, service string, createTable bool) (*TableClien func initServiceTest(t *testing.T, service string) (*TableServiceClient, func()) { var client *TableServiceClient var err error - if service == string(StorageEndpoint) { + if service == string(storageEndpoint) { client, err = createStorageServiceClient(t) require.NoError(t, err) - } else if service == string(CosmosEndpoint) { + } else if service == string(cosmosEndpoint) { client, err = createCosmosServiceClient(t) require.NoError(t, err) } @@ -226,7 +226,7 @@ func clearAllTables(service *TableServiceClient) error { pager := service.ListTables(nil) for pager.NextPage(ctx) { resp := pager.PageResponse() - for _, v := range resp.TableQueryResponse.Value { + for _, v := range resp.TableListResponse.Value { _, err := service.DeleteTable(ctx, *v.TableName, nil) if err != nil { return err diff --git a/sdk/tables/aztable/recordings/TestUpsertEntity/TestUpsertEntity_cosmos.json b/sdk/tables/aztable/recordings/TestInsertEntity/TestInsertEntity_cosmos.json similarity index 89% rename from sdk/tables/aztable/recordings/TestUpsertEntity/TestUpsertEntity_cosmos.json rename to sdk/tables/aztable/recordings/TestInsertEntity/TestInsertEntity_cosmos.json index 3c5d71b33d10..bdf17f03f44c 100644 --- a/sdk/tables/aztable/recordings/TestUpsertEntity/TestUpsertEntity_cosmos.json +++ b/sdk/tables/aztable/recordings/TestInsertEntity/TestInsertEntity_cosmos.json @@ -19,29 +19,29 @@ "x-ms-version": "2019-02-02" }, "RequestBody": { - "TableName": "tableName2941442851" + "TableName": "tableName3858182091" }, "StatusCode": 201, "ResponseHeaders": { "Content-Type": "application/json; odata=minimalmetadata", "Date": "Mon, 02 Aug 2021 18:01:58 GMT", "ETag": "W/\u0022datetime\u00272021-08-02T18%3A01%3A58.7090440Z\u0027\u0022", - "Location": "https://seankaneprim.table.cosmos.azure.com/Tables(\u0027tableName2941442851\u0027)", + "Location": "https://seankaneprim.table.cosmos.azure.com/Tables(\u0027tableName3858182091\u0027)", "Transfer-Encoding": "chunked", "x-ms-request-id": "482077b6-f28f-4226-b3f7-e577806e4b28" }, "ResponseBody": { - "TableName": "tableName2941442851", + "TableName": "tableName3858182091", "odata.metadata": "https://seankaneprim.table.cosmos.azure.com/$metadata#Tables/@Element" } }, { - "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName2941442851(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName3858182091(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", "RequestMethod": "PUT", "RequestHeaders": { ":authority": "localhost:5001", ":method": "PUT", - ":path": "/tableName2941442851(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + ":path": "/tableName3858182091(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", ":scheme": "https", "Accept": "application/json", "Accept-Encoding": "gzip", @@ -71,12 +71,12 @@ "ResponseBody": null }, { - "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName2941442851(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName3858182091(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", "RequestMethod": "GET", "RequestHeaders": { ":authority": "localhost:5001", ":method": "GET", - ":path": "/tableName2941442851(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + ":path": "/tableName3858182091(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", ":scheme": "https", "Accept": "application/json;odata=minimalmetadata", "Accept-Encoding": "gzip", @@ -96,7 +96,7 @@ "x-ms-request-id": "a1f6a398-213c-4098-ba60-2ea73fae92f6" }, "ResponseBody": { - "odata.metadata": "https://seankaneprim.table.cosmos.azure.com/tableName2941442851/$metadata#tableName2941442851/@Element", + "odata.metadata": "https://seankaneprim.table.cosmos.azure.com/tableName3858182091/$metadata#tableName3858182091/@Element", "odata.etag": "W/\u0022datetime\u00272021-08-02T18%3A01%3A59.3616392Z\u0027\u0022", "PartitionKey": "partition", "RowKey": "1", @@ -107,12 +107,12 @@ } }, { - "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName2941442851(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName3858182091(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", "RequestMethod": "PUT", "RequestHeaders": { ":authority": "localhost:5001", ":method": "PUT", - ":path": "/tableName2941442851(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + ":path": "/tableName3858182091(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", ":scheme": "https", "Accept": "application/json", "Accept-Encoding": "gzip", @@ -141,12 +141,12 @@ "ResponseBody": null }, { - "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName2941442851()?%24filter=RowKey\u002Beq\u002B%271%27", + "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/tableName3858182091()?%24filter=RowKey\u002Beq\u002B%271%27", "RequestMethod": "GET", "RequestHeaders": { ":authority": "localhost:5001", ":method": "GET", - ":path": "/tableName2941442851()?%24filter=RowKey\u002Beq\u002B%271%27", + ":path": "/tableName3858182091()?%24filter=RowKey\u002Beq\u002B%271%27", ":scheme": "https", "Accept": "application/json;odata=minimalmetadata", "Accept-Encoding": "gzip", @@ -176,16 +176,16 @@ "Timestamp": "2021-08-02T18:01:59.6248072Z" } ], - "odata.metadata": "https://seankaneprim.table.cosmos.azure.com/$metadata#tableName2941442851" + "odata.metadata": "https://seankaneprim.table.cosmos.azure.com/$metadata#tableName3858182091" } }, { - "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/Tables(\u0027tableName2941442851\u0027)", + "RequestUri": "https://fakestorageaccount.table.cosmos.azure.com/Tables(\u0027tableName3858182091\u0027)", "RequestMethod": "DELETE", "RequestHeaders": { ":authority": "localhost:5001", ":method": "DELETE", - ":path": "/Tables(\u0027tableName2941442851\u0027)", + ":path": "/Tables(\u0027tableName3858182091\u0027)", ":scheme": "https", "Accept": "application/json", "Accept-Encoding": "gzip", diff --git a/sdk/tables/aztable/recordings/TestUpsertEntity/TestUpsertEntity_storage.json b/sdk/tables/aztable/recordings/TestInsertEntity/TestInsertEntity_storage.json similarity index 91% rename from sdk/tables/aztable/recordings/TestUpsertEntity/TestUpsertEntity_storage.json rename to sdk/tables/aztable/recordings/TestInsertEntity/TestInsertEntity_storage.json index e70a1edd4256..830e3e68491c 100644 --- a/sdk/tables/aztable/recordings/TestUpsertEntity/TestUpsertEntity_storage.json +++ b/sdk/tables/aztable/recordings/TestInsertEntity/TestInsertEntity_storage.json @@ -19,14 +19,14 @@ "x-ms-version": "2019-02-02" }, "RequestBody": { - "TableName": "tableName4103702124" + "TableName": "tableName2262274532" }, "StatusCode": 201, "ResponseHeaders": { "Cache-Control": "no-cache", "Content-Type": "application/json; odata=minimalmetadata; streaming=true; charset=utf-8", "Date": "Mon, 02 Aug 2021 18:01:56 GMT", - "Location": "https://seankaneprim.table.core.windows.net/Tables(\u0027tableName4103702124\u0027)", + "Location": "https://seankaneprim.table.core.windows.net/Tables(\u0027tableName2262274532\u0027)", "Server": [ "Windows-Azure-Table/1.0", "Microsoft-HTTPAPI/2.0" @@ -38,16 +38,16 @@ }, "ResponseBody": { "odata.metadata": "https://seankaneprim.table.core.windows.net/$metadata#Tables/@Element", - "TableName": "tableName4103702124" + "TableName": "tableName2262274532" } }, { - "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName4103702124(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName2262274532(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", "RequestMethod": "PUT", "RequestHeaders": { ":authority": "localhost:5001", ":method": "PUT", - ":path": "/tableName4103702124(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + ":path": "/tableName2262274532(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", ":scheme": "https", "Accept": "application/json", "Accept-Encoding": "gzip", @@ -84,12 +84,12 @@ "ResponseBody": null }, { - "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName4103702124(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName2262274532(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", "RequestMethod": "GET", "RequestHeaders": { ":authority": "localhost:5001", ":method": "GET", - ":path": "/tableName4103702124(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + ":path": "/tableName2262274532(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", ":scheme": "https", "Accept": "application/json;odata=minimalmetadata", "Accept-Encoding": "gzip", @@ -117,7 +117,7 @@ "x-ms-version": "2019-02-02" }, "ResponseBody": { - "odata.metadata": "https://seankaneprim.table.core.windows.net/$metadata#tableName4103702124/@Element", + "odata.metadata": "https://seankaneprim.table.core.windows.net/$metadata#tableName2262274532/@Element", "odata.etag": "W/\u0022datetime\u00272021-08-02T18%3A01%3A57.8187862Z\u0027\u0022", "PartitionKey": "partition", "RowKey": "1", @@ -128,12 +128,12 @@ } }, { - "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName4103702124(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName2262274532(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", "RequestMethod": "PUT", "RequestHeaders": { ":authority": "localhost:5001", ":method": "PUT", - ":path": "/tableName4103702124(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", + ":path": "/tableName2262274532(PartitionKey=\u0027partition\u0027,RowKey=\u00271\u0027)", ":scheme": "https", "Accept": "application/json", "Accept-Encoding": "gzip", @@ -169,12 +169,12 @@ "ResponseBody": null }, { - "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName4103702124()?%24filter=RowKey\u002Beq\u002B%271%27", + "RequestUri": "https://fakestorageaccount.table.core.windows.net/tableName2262274532()?%24filter=RowKey\u002Beq\u002B%271%27", "RequestMethod": "GET", "RequestHeaders": { ":authority": "localhost:5001", ":method": "GET", - ":path": "/tableName4103702124()?%24filter=RowKey\u002Beq\u002B%271%27", + ":path": "/tableName2262274532()?%24filter=RowKey\u002Beq\u002B%271%27", ":scheme": "https", "Accept": "application/json;odata=minimalmetadata", "Accept-Encoding": "gzip", @@ -201,7 +201,7 @@ "x-ms-version": "2019-02-02" }, "ResponseBody": { - "odata.metadata": "https://seankaneprim.table.core.windows.net/$metadata#tableName4103702124", + "odata.metadata": "https://seankaneprim.table.core.windows.net/$metadata#tableName2262274532", "value": [ { "odata.etag": "W/\u0022datetime\u00272021-08-02T18%3A01%3A58.0749701Z\u0027\u0022", @@ -216,12 +216,12 @@ } }, { - "RequestUri": "https://fakestorageaccount.table.core.windows.net/Tables(\u0027tableName4103702124\u0027)", + "RequestUri": "https://fakestorageaccount.table.core.windows.net/Tables(\u0027tableName2262274532\u0027)", "RequestMethod": "DELETE", "RequestHeaders": { ":authority": "localhost:5001", ":method": "DELETE", - ":path": "/Tables(\u0027tableName4103702124\u0027)", + ":path": "/Tables(\u0027tableName2262274532\u0027)", ":scheme": "https", "Accept": "application/json", "Accept-Encoding": "gzip", diff --git a/sdk/tables/aztable/sas_account.go b/sdk/tables/aztable/sas_account.go new file mode 100644 index 000000000000..664b40481c12 --- /dev/null +++ b/sdk/tables/aztable/sas_account.go @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package aztable + +import ( + "bytes" + "errors" + "fmt" + "strings" + "time" +) + +var SASVersion = "2019-02-02" + +// AccountSASSignatureValues 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 AccountSASSignatureValues 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 + Permissions string `param:"sp"` // Create by initializing a AccountSASPermissions and then call String() + IPRange IPRange `param:"sip"` + Services string `param:"ss"` // Create by initializing AccountSASServices and then call String() + ResourceTypes string `param:"srt"` // Create by initializing AccountSASResourceTypes and then call String() +} + +// NewSASQueryParameters uses an account's SharedKeyCredential to sign this signature values to produce +// the proper SAS query parameters. +func (v AccountSASSignatureValues) NewSASQueryParameters(sharedKeyCredential *SharedKeyCredential) (SASQueryParameters, 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 SASQueryParameters{}, errors.New("account SAS is missing at least one of these: ExpiryTime, Permissions, Service, or ResourceType") + } + if v.Version == "" { + v.Version = SASVersion + } + perms := &AccountSASPermissions{} + if err := perms.Parse(v.Permissions); err != nil { + return SASQueryParameters{}, err + } + v.Permissions = perms.String() + + startTime, expiryTime := FormatTimesForSASSigning(v.StartTime, v.ExpiryTime) + + stringToSign := strings.Join([]string{ + sharedKeyCredential.AccountName(), + v.Permissions, + v.Services, + v.ResourceTypes, + startTime, + expiryTime, + v.IPRange.String(), + string(v.Protocol), + v.Version, + ""}, // That right, the account SAS requires a terminating extra newline + "\n") + + signature, err := sharedKeyCredential.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, + + // Account-specific SAS parameters + services: v.Services, + resourceTypes: v.ResourceTypes, + + // Calculated SAS signature + signature: signature, + } + + return p, nil +} + +// The AccountSASPermissions 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 AccountSASPermissions struct { + Read, Write, Delete, List, Add, Create, Update, Process bool +} + +// String produces the SAS permissions string for an Azure Storage account. +// Call this method to set AccountSASSignatureValues's Permissions field. +func (p AccountSASPermissions) String() string { + var buffer bytes.Buffer + if p.Read { + buffer.WriteRune('r') + } + if p.Write { + buffer.WriteRune('w') + } + if p.Delete { + buffer.WriteRune('d') + } + if p.List { + buffer.WriteRune('l') + } + if p.Add { + buffer.WriteRune('a') + } + if p.Create { + buffer.WriteRune('c') + } + if p.Update { + buffer.WriteRune('u') + } + if p.Process { + buffer.WriteRune('p') + } + return buffer.String() +} + +// Parse initializes the AccountSASPermissions's fields from a string. +func (p *AccountSASPermissions) Parse(s string) error { + *p = AccountSASPermissions{} // Clear out the flags + for _, r := range s { + switch r { + case 'r': + p.Read = true + case 'w': + p.Write = true + case 'd': + p.Delete = true + case 'l': + p.List = true + case 'a': + p.Add = true + case 'c': + p.Create = true + case 'u': + p.Update = true + case 'p': + p.Process = true + case 'x': + p.Process = true + default: + return fmt.Errorf("invalid permission character: '%v'", r) + } + } + return nil +} + +// The AccountSASResourceTypes type simplifies creating the resource types string for an Azure Storage Account SAS. +// Initialize an instance of this type and then call its String method to set AccountSASSignatureValues's ResourceTypes field. +type AccountSASResourceTypes struct { + Service, Container, Object bool +} + +// String produces the SAS resource types string for an Azure Storage account. +// Call this method to set AccountSASSignatureValues's ResourceTypes field. +func (rt AccountSASResourceTypes) String() string { + var buffer bytes.Buffer + if rt.Service { + buffer.WriteRune('s') + } + if rt.Container { + buffer.WriteRune('c') + } + if rt.Object { + buffer.WriteRune('o') + } + return buffer.String() +} + +// Parse initializes the AccountSASResourceType's fields from a string. +func (rt *AccountSASResourceTypes) Parse(s string) error { + *rt = AccountSASResourceTypes{} // Clear out the flags + for _, r := range s { + switch r { + case 's': + rt.Service = true + case 'c': + rt.Container = true + case 'o': + rt.Object = true + default: + return fmt.Errorf("invalid resource type: '%v'", r) + } + } + return nil +} diff --git a/sdk/tables/aztable/sas_query_params.go b/sdk/tables/aztable/sas_query_params.go new file mode 100644 index 000000000000..f313f74adfb7 --- /dev/null +++ b/sdk/tables/aztable/sas_query_params.go @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package aztable + +import ( + "errors" + "net" + "net/url" + "strings" + "time" +) + +// SASVersion indicates the SAS version. +type SASProtocol string + +const ( + // SASProtocolHTTPS can be specified for a SAS protocol + SASProtocolHTTPS SASProtocol = "https" + + // SASProtocolHTTPSandHTTP can be specified for a SAS protocol + SASProtocolHTTPSandHTTP SASProtocol = "https,http" +) + +const SnapshotTimeFormat = "2006-01-02T15:04:05.0000000Z07:00" + +// FormatTimesForSASSigning converts a time.Time to a snapshotTimeFormat string suitable for a +// SASField's StartTime or ExpiryTime fields. Returns "" if value.IsZero(). +func FormatTimesForSASSigning(startTime, expiryTime time.Time) (string, string) { + ss := "" + if !startTime.IsZero() { + ss = formatSASTimeWithDefaultFormat(&startTime) + } + se := "" + if !expiryTime.IsZero() { + se = formatSASTimeWithDefaultFormat(&expiryTime) + } + return ss, se +} + +// SASTimeFormat represents the format of a SAS start or expiry time. Use it when formatting/parsing a time.Time. +const SASTimeFormat = "2006-01-02T15:04:05Z" //"2017-07-27T00:00:00Z" // ISO 8601 +var SASTimeFormats = []string{"2006-01-02T15:04:05.0000000Z", SASTimeFormat, "2006-01-02T15:04Z", "2006-01-02"} // ISO 8601 formats, please refer to https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas for more details. + +// formatSASTimeWithDefaultFormat format time with ISO 8601 in "yyyy-MM-ddTHH:mm:ssZ". +func formatSASTimeWithDefaultFormat(t *time.Time) string { + return formatSASTime(t, SASTimeFormat) // By default, "yyyy-MM-ddTHH:mm:ssZ" is used +} + +// formatSASTime format time with given format, use ISO 8601 in "yyyy-MM-ddTHH:mm:ssZ" by default. +func formatSASTime(t *time.Time, format string) string { + if format != "" { + return t.Format(format) + } + return t.Format(SASTimeFormat) // By default, "yyyy-MM-ddTHH:mm:ssZ" is used +} + +// parseSASTimeString try to parse sas time string. +func parseSASTimeString(val string) (t time.Time, timeFormat string, err error) { + for _, sasTimeFormat := range SASTimeFormats { + t, err = time.Parse(sasTimeFormat, val) + if err == nil { + timeFormat = sasTimeFormat + break + } + } + + if err != nil { + err = errors.New("fail to parse time with IOS 8601 formats, please refer to https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas for more details") + } + + return +} + +// https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas + +// A SASQueryParameters object represents the components that make up an Azure Storage SAS' query parameters. +// You parse a map of query parameters into its fields by calling NewSASQueryParameters(). You add the components +// to a query parameter map by calling AddToValues(). +// NOTE: Changing any field requires computing a new SAS signature using a XxxSASSignatureValues type. +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"` + ipRange IPRange `param:"sip"` + identifier string `param:"si"` + resource string `param:"sr"` + permissions string `param:"sp"` + signature string `param:"sig"` + signedVersion string `param:"skv"` + tableName string `param:"tn"` + startPk string `param:"spk"` + startRk string `param:"srk"` + endPk string `param:"epk"` + endRk string `param:"erk"` + + // private member used for startTime and expiryTime formatting. + stTimeFormat string + seTimeFormat string +} + +func (p *SASQueryParameters) SignedVersion() string { + return p.signedVersion +} + +func (p *SASQueryParameters) Version() string { + return p.version +} + +func (p *SASQueryParameters) Services() string { + return p.services +} +func (p *SASQueryParameters) ResourceTypes() string { + return p.resourceTypes +} +func (p *SASQueryParameters) Protocol() SASProtocol { + return p.protocol +} +func (p *SASQueryParameters) StartTime() time.Time { + return p.startTime +} +func (p *SASQueryParameters) ExpiryTime() time.Time { + return p.expiryTime +} + +func (p *SASQueryParameters) IPRange() IPRange { + return p.ipRange +} + +func (p *SASQueryParameters) Identifier() string { + return p.identifier +} + +func (p *SASQueryParameters) Resource() string { + return p.resource +} +func (p *SASQueryParameters) Permissions() string { + return p.permissions +} + +func (p *SASQueryParameters) Signature() string { + return p.signature +} + +func (p *SASQueryParameters) StartPartitionKey() string { + return p.startPk +} + +func (p *SASQueryParameters) StartRowKey() string { + return p.startRk +} + +func (p *SASQueryParameters) EndPartitionKey() string { + return p.endPk +} + +func (p *SASQueryParameters) EndRowKey() string { + return p.endRk +} + +// IPRange represents a SAS IP range's start IP and (optionally) end IP. +type IPRange struct { + Start net.IP // Not specified if length = 0 + End net.IP // Not specified if length = 0 +} + +// String returns a string representation of an IPRange. +func (ipr *IPRange) String() string { + if len(ipr.Start) == 0 { + return "" + } + start := ipr.Start.String() + if len(ipr.End) == 0 { + return start + } + return start + "-" + ipr.End.String() +} + +// NewSASQueryParameters creates and initializes a SASQueryParameters object based on the +// query parameter map's passed-in values. If deleteSASParametersFromValues is true, +// all SAS-related query parameters are removed from the passed-in map. If +// deleteSASParametersFromValues is false, the map passed-in map is unaltered. +func newSASQueryParameters(values url.Values, deleteSASParametersFromValues bool) SASQueryParameters { + p := SASQueryParameters{} + for k, v := range values { + val := v[0] + isSASKey := true + switch strings.ToLower(k) { + case "sv": + p.version = val + case "ss": + p.services = val + case "srt": + p.resourceTypes = val + case "spr": + p.protocol = SASProtocol(val) + case "st": + p.startTime, p.stTimeFormat, _ = parseSASTimeString(val) + case "se": + p.expiryTime, p.seTimeFormat, _ = parseSASTimeString(val) + case "sip": + dashIndex := strings.Index(val, "-") + if dashIndex == -1 { + p.ipRange.Start = net.ParseIP(val) + } else { + p.ipRange.Start = net.ParseIP(val[:dashIndex]) + p.ipRange.End = net.ParseIP(val[dashIndex+1:]) + } + case "si": + p.identifier = val + case "sr": + p.resource = val + case "sp": + p.permissions = val + case "sig": + p.signature = val + case "skv": + p.signedVersion = val + case "spk": + p.startPk = val + case "epk": + p.endPk = val + case "srk": + p.startRk = val + case "erk": + p.endRk = val + default: + isSASKey = false // We didn't recognize the query parameter + } + if isSASKey && deleteSASParametersFromValues { + delete(values, k) + } + } + return p +} + +// addToValues adds the SAS components to the specified query parameters map. +func (p *SASQueryParameters) addToValues(v url.Values) url.Values { + if p.version != "" { + v.Add("sv", p.version) + } + if p.services != "" { + v.Add("ss", p.services) + } + if p.resourceTypes != "" { + v.Add("srt", p.resourceTypes) + } + if p.protocol != "" { + v.Add("spr", string(p.protocol)) + } + if !p.startTime.IsZero() { + v.Add("st", formatSASTime(&(p.startTime), p.stTimeFormat)) + } + if !p.expiryTime.IsZero() { + v.Add("se", formatSASTime(&(p.expiryTime), p.seTimeFormat)) + } + if len(p.ipRange.Start) > 0 { + v.Add("sip", p.ipRange.String()) + } + if p.identifier != "" { + v.Add("si", p.identifier) + } + if p.resource != "" { + v.Add("sr", p.resource) + } + if p.permissions != "" { + v.Add("sp", p.permissions) + } + if p.signature != "" { + v.Add("sig", p.signature) + } + if p.tableName != "" { + v.Add("tn", p.tableName) + } + if p.startPk != "" { + v.Add("spk", p.startPk) + } + if p.endPk != "" { + v.Add("epk", p.endPk) + } + if p.startRk != "" { + v.Add("srk", p.startRk) + } + if p.endRk != "" { + v.Add("erk", p.endRk) + } + return v +} + +// Encode encodes the SAS query parameters into URL encoded form sorted by key. +func (p *SASQueryParameters) Encode() string { + v := url.Values{} + p.addToValues(v) + return v.Encode() +} diff --git a/sdk/tables/aztable/sas_service.go b/sdk/tables/aztable/sas_service.go new file mode 100644 index 000000000000..4d89de18a927 --- /dev/null +++ b/sdk/tables/aztable/sas_service.go @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package aztable + +import ( + "bytes" + "fmt" + "strings" + "time" +) + +// TableSASSignatureValues is used to generate a Shared Access Signature (SAS) for an Azure Table instance. +// For more information, see https://docs.microsoft.com/rest/api/storageservices/constructing-a-service-sas +type TableSASSignatureValues struct { + Version string // If not specified, this defaults to SASVersion + Protocol SASProtocol // See the SASProtocol* constants + StartTime time.Time // Not specified if IsZero + ExpiryTime time.Time // Not specified if IsZero + Permissions string // Create by initializing a ContainerSASPermissions or TableSASPermissions and then call String() + IPRange IPRange + Identifier string + TableName string + StartPartitionKey string + StartRowKey string + EndPartitionKey string + EndRowKey string +} + +// NewSASQueryParameters uses an account's SharedKeyCredential to sign this signature values to produce +// the proper SAS query parameters. +func (v TableSASSignatureValues) NewSASQueryParameters(credential *SharedKeyCredential) (SASQueryParameters, error) { + resource := "" + + if v.Version != "" { + //Make sure the permission characters are in the correct order + perms := &TableSASPermissions{} + if err := perms.Parse(v.Permissions); err != nil { + return SASQueryParameters{}, err + } + v.Permissions = perms.String() + } else if v.TableName == "" { + // Make sure the permission characters are in the correct order + perms := &TableSASPermissions{} + if err := perms.Parse(v.Permissions); err != nil { + return SASQueryParameters{}, err + } + v.Permissions = perms.String() + } else { + // Make sure the permission characters are in the correct order + perms := &TableSASPermissions{} + if err := perms.Parse(v.Permissions); err != nil { + return SASQueryParameters{}, err + } + v.Permissions = perms.String() + } + if v.Version == "" { + v.Version = SASVersion + } + startTime, expiryTime := FormatTimesForSASSigning(v.StartTime, v.ExpiryTime) + + signedIdentifier := v.Identifier + + lowerCaseTableName := strings.ToLower(v.TableName) + + p := SASQueryParameters{ + // Common SAS parameters + version: v.Version, + protocol: v.Protocol, + startTime: v.StartTime, + expiryTime: v.ExpiryTime, + permissions: v.Permissions, + ipRange: v.IPRange, + tableName: lowerCaseTableName, + + // Table SAS parameters + resource: resource, + identifier: v.Identifier, + } + + canonicalName := "/" + "table" + "/" + credential.AccountName() + "/" + lowerCaseTableName + + // String to sign: http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx + stringToSign := strings.Join([]string{ + v.Permissions, + startTime, + expiryTime, + canonicalName, + signedIdentifier, + v.IPRange.String(), + string(v.Protocol), + v.Version, + v.StartPartitionKey, + v.StartRowKey, + v.EndPartitionKey, + v.EndRowKey, + }, + "\n", + ) + + signature, err := credential.ComputeHMACSHA256(stringToSign) + p.signature = signature + return p, err +} + +// The TableSASPermissions type simplifies creating the permissions string for an Azure Table. +// Initialize an instance of this type and then call its String method to set TableSASSignatureValues's Permissions field. +type TableSASPermissions struct { + Read bool + Add bool + Update bool + Delete bool + StartPartitionKey string + StartRowKey string + EndPartitionKey string + EndRowKey string +} + +// String produces the SAS permissions string for an Azure Storage blob. +// Call this method to set TableSASSignatureValues's Permissions field. +func (p TableSASPermissions) String() string { + var b bytes.Buffer + if p.Read { + b.WriteRune('r') + } + if p.Add { + b.WriteRune('a') + } + if p.Update { + b.WriteRune('u') + } + if p.Delete { + b.WriteRune('d') + } + return b.String() +} + +// Parse initializes the TableSASPermissions's fields from a string. +func (p *TableSASPermissions) Parse(s string) error { + *p = TableSASPermissions{} // Clear the flags + for _, r := range s { + switch r { + case 'r': + p.Read = true + case 'a': + p.Add = true + case 'u': + p.Update = true + case 'd': + p.Delete = true + default: + return fmt.Errorf("invalid permission: '%v'", r) + } + } + return nil +} diff --git a/sdk/tables/aztable/shared_access_signature_test.go b/sdk/tables/aztable/shared_access_signature_test.go new file mode 100644 index 000000000000..124af26e2aa3 --- /dev/null +++ b/sdk/tables/aztable/shared_access_signature_test.go @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package aztable + +import ( + "context" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/internal/recording" + "github.com/stretchr/testify/require" +) + +func TestSASServiceClient(t *testing.T) { + recording.LiveOnly(t) + accountName := os.Getenv("TABLES_PRIMARY_ACCOUNT_NAME") + accountKey := os.Getenv("TABLES_PRIMARY_STORAGE_ACCOUNT_KEY") + cred, err := NewSharedKeyCredential(accountName, accountKey) + require.NoError(t, err) + + serviceClient, err := NewTableServiceClient(fmt.Sprintf("https://%s.table.core.windows.net/", accountName), cred, nil) + require.NoError(t, err) + + tableName, err := createRandomName(t, "tableName") + require.NoError(t, err) + + delete := func() { + _, err := serviceClient.DeleteTable(context.Background(), tableName, nil) + require.NoError(t, err) + } + defer delete() + + _, err = serviceClient.CreateTable(context.Background(), tableName) + require.NoError(t, err) + + resources := AccountSASResourceTypes{ + Object: true, + Service: true, + Container: true, + } + permissions := AccountSASPermissions{ + Read: true, + Add: true, + Write: true, + } + start := time.Date(2021, time.August, 4, 1, 1, 0, 0, time.UTC) + expiry := time.Date(2022, time.August, 4, 1, 1, 0, 0, time.UTC) + + accountSAS, err := serviceClient.GetAccountSASToken(resources, permissions, start, expiry) + require.NoError(t, err) + + queryParams := accountSAS.Encode() + + sasUrl := fmt.Sprintf("https://%s.table.core.windows.net/?%s", accountName, queryParams) + + err = recording.StartRecording(t, nil) + require.NoError(t, err) + client, err := createTableClientForRecording(t, tableName, sasUrl, azcore.AnonymousCredential()) + require.NoError(t, err) + defer recording.StopRecording(t, nil) + + entity := map[string]string{ + "PartitionKey": "pk001", + "RowKey": "rk001", + "Value": "5", + } + marshalled, err := json.Marshal(entity) + require.NoError(t, err) + + _, err = client.AddEntity(context.Background(), marshalled) + require.NoError(t, err) +} + +func TestSASTableClient(t *testing.T) { + recording.LiveOnly(t) + accountName := os.Getenv("TABLES_PRIMARY_ACCOUNT_NAME") + accountKey := os.Getenv("TABLES_PRIMARY_STORAGE_ACCOUNT_KEY") + cred, err := NewSharedKeyCredential(accountName, accountKey) + require.NoError(t, err) + + serviceClient, err := NewTableServiceClient(fmt.Sprintf("https://%s.table.core.windows.net/", accountName), cred, nil) + require.NoError(t, err) + + tableName, err := createRandomName(t, "tableName") + require.NoError(t, err) + tableName = "tablename" + + delete := func() { + _, err := serviceClient.DeleteTable(context.Background(), tableName, nil) + require.NoError(t, err) + } + defer delete() + + _, err = serviceClient.CreateTable(context.Background(), tableName) + require.NoError(t, err) + + permissions := TableSASPermissions{ + Read: true, + Add: true, + } + start := time.Date(2021, time.August, 4, 1, 1, 0, 0, time.UTC) + expiry := time.Date(2022, time.August, 4, 1, 1, 0, 0, time.UTC) + + accountSAS, err := serviceClient.GetTableSASToken(tableName, permissions, start, expiry) + require.NoError(t, err) + + queryParams := accountSAS.Encode() + + sasUrl := fmt.Sprintf("https://%s.table.core.windows.net/?%s", accountName, queryParams) + + err = recording.StartRecording(t, nil) + require.NoError(t, err) + client, err := createTableClientForRecording(t, tableName, sasUrl, azcore.AnonymousCredential()) + require.NoError(t, err) + defer recording.StopRecording(t, nil) + + entity := map[string]string{ + "PartitionKey": "pk001", + "RowKey": "rk001", + "Value": "5", + } + marshalled, err := json.Marshal(entity) + require.NoError(t, err) + + _, err = client.AddEntity(context.Background(), marshalled) + require.NoError(t, err) +} + +func TestSASTableClientReadOnly(t *testing.T) { + recording.LiveOnly(t) + accountName := os.Getenv("TABLES_PRIMARY_ACCOUNT_NAME") + accountKey := os.Getenv("TABLES_PRIMARY_STORAGE_ACCOUNT_KEY") + cred, err := NewSharedKeyCredential(accountName, accountKey) + require.NoError(t, err) + + serviceClient, err := NewTableServiceClient(fmt.Sprintf("https://%s.table.core.windows.net/", accountName), cred, nil) + require.NoError(t, err) + + tableName, err := createRandomName(t, "tableName") + require.NoError(t, err) + + delete := func() { + _, err := serviceClient.DeleteTable(context.Background(), tableName, nil) + require.NoError(t, err) + } + defer delete() + + _, err = serviceClient.CreateTable(context.Background(), tableName) + require.NoError(t, err) + + client := serviceClient.NewTableClient(tableName) + err = insertNEntities("pk001", 4, client) + require.NoError(t, err) + + permissions := TableSASPermissions{ + Read: true, + } + start := time.Date(2021, time.August, 4, 1, 1, 0, 0, time.UTC) + expiry := time.Date(2022, time.August, 4, 1, 1, 0, 0, time.UTC) + + accountSAS, err := serviceClient.GetTableSASToken(tableName, permissions, start, expiry) + require.NoError(t, err) + + queryParams := accountSAS.Encode() + + sasUrl := fmt.Sprintf("https://%s.table.core.windows.net/?%s", accountName, queryParams) + + err = recording.StartRecording(t, nil) + require.NoError(t, err) + client, err = createTableClientForRecording(t, tableName, sasUrl, azcore.AnonymousCredential()) + require.NoError(t, err) + defer recording.StopRecording(t, nil) + + entity := map[string]string{ + "PartitionKey": "pk001", + "RowKey": "rk001", + "Value": "5", + } + marshalled, err := json.Marshal(entity) + require.NoError(t, err) + + // Failure on a read + _, err = client.AddEntity(context.Background(), marshalled) + require.Error(t, err) + + // Success on a list + pager := client.List(nil) + count := 0 + for pager.NextPage(context.Background()) { + count += len(pager.PageResponse().TableEntityQueryResponse.Value) + } + + require.Equal(t, 4, count) +} + +func TestSASCosmosTableClientReadOnly(t *testing.T) { + recording.LiveOnly(t) + accountName := os.Getenv("TABLES_COSMOS_ACCOUNT_NAME") + accountKey := os.Getenv("TABLES_PRIMARY_COSMOS_ACCOUNT_KEY") + cred, err := NewSharedKeyCredential(accountName, accountKey) + require.NoError(t, err) + + serviceClient, err := NewTableServiceClient(fmt.Sprintf("https://%s.table.cosmos.azure.com/", accountName), cred, nil) + require.NoError(t, err) + + tableName, err := createRandomName(t, "tableName") + require.NoError(t, err) + + delete := func() { + _, err := serviceClient.DeleteTable(context.Background(), tableName, nil) + require.NoError(t, err) + } + defer delete() + + _, err = serviceClient.CreateTable(context.Background(), tableName) + require.NoError(t, err) + + client := serviceClient.NewTableClient(tableName) + err = insertNEntities("pk001", 4, client) + require.NoError(t, err) + + permissions := TableSASPermissions{ + Read: true, + } + start := time.Date(2021, time.August, 4, 1, 1, 0, 0, time.UTC) + expiry := time.Date(2022, time.August, 4, 1, 1, 0, 0, time.UTC) + accountSAS, err := serviceClient.GetTableSASToken(tableName, permissions, start, expiry) + require.NoError(t, err) + + queryParams := accountSAS.Encode() + + sasUrl := fmt.Sprintf("https://%s.table.cosmos.azure.com/?%s", accountName, queryParams) + + err = recording.StartRecording(t, nil) + require.NoError(t, err) + client, err = createTableClientForRecording(t, tableName, sasUrl, azcore.AnonymousCredential()) + require.NoError(t, err) + defer recording.StopRecording(t, nil) + + entity := map[string]string{ + "PartitionKey": "pk001", + "RowKey": "rk001", + "Value": "5", + } + marshalled, err := json.Marshal(entity) + require.NoError(t, err) + + // Failure on a read + _, err = client.AddEntity(context.Background(), marshalled) + require.Error(t, err) + + // Success on a list + pager := client.List(nil) + count := 0 + for pager.NextPage(context.Background()) { + count += len(pager.PageResponse().TableEntityQueryResponse.Value) + } + + require.Equal(t, 4, count) +} diff --git a/sdk/tables/aztable/table_batch_test.go b/sdk/tables/aztable/table_batch_test.go index 6336ee4bfebc..7a7fbde8a65e 100644 --- a/sdk/tables/aztable/table_batch_test.go +++ b/sdk/tables/aztable/table_batch_test.go @@ -76,7 +76,7 @@ func TestBatchMixed(t *testing.T) { require.Equal(t, http.StatusNoContent, r.StatusCode) } - var qResp TableEntityQueryByteResponseResponse + var qResp TableEntityListByteResponseResponse filter := "RowKey eq '1'" list := &ListOptions{Filter: &filter} pager := client.List(list) @@ -108,7 +108,7 @@ func TestBatchMixed(t *testing.T) { require.NoError(t, err) batch[1] = TableTransactionAction{ActionType: Delete, Entity: marshalledSecondEntity} - // create an upsert action to replace the third added entity with a new value + // create an insert action to replace the third added entity with a new value replaceProp := "ReplaceProperty" var replaceProperties = map[string]interface{}{ partitionKey: (*entitiesToCreate)[2].PartitionKey, @@ -117,7 +117,7 @@ func TestBatchMixed(t *testing.T) { } marshalledThirdEntity, err := json.Marshal(replaceProperties) require.NoError(t, err) - batch[2] = TableTransactionAction{ActionType: UpsertReplace, Entity: marshalledThirdEntity} + batch[2] = TableTransactionAction{ActionType: InsertReplace, Entity: marshalledThirdEntity} // Add the remaining 2 entities. marshalled4thEntity, err := json.Marshal((*entitiesToCreate)[3]) @@ -255,14 +255,14 @@ func TestBatchComplex(t *testing.T) { marshalled1, err = json.Marshal(edmEntity) require.NoError(t, err) batch2[0] = TableTransactionAction{ - ActionType: UpsertMerge, + ActionType: InsertMerge, Entity: marshalled1, } marshalled2, err = json.Marshal(edmEntity2) require.NoError(t, err) batch2[1] = TableTransactionAction{ - ActionType: UpsertReplace, + ActionType: InsertReplace, Entity: marshalled2, } diff --git a/sdk/tables/aztable/table_client.go b/sdk/tables/aztable/table_client.go index 4c40b7df4f91..144e4ab1ea5e 100644 --- a/sdk/tables/aztable/table_client.go +++ b/sdk/tables/aztable/table_client.go @@ -6,7 +6,6 @@ package aztable import ( "context" "encoding/json" - "errors" "github.com/Azure/azure-sdk-for-go/sdk/azcore" ) @@ -19,6 +18,8 @@ type TableClient struct { Name string } +// EntityUpdateMode specifies what type of update to do on InsertEntity or UpdateEntity. ReplaceEntity +// will replace an existing entity, MergeEntity will merge properties of the entities. type EntityUpdateMode string const ( @@ -56,17 +57,19 @@ func (t *TableClient) Delete(ctx context.Context, options *TableDeleteOptions) ( // // List returns a Pager, which allows iteration through each page of results. Example: // -// options := &ListOptions{Filter: to.StringPtr("PartitionKey eq 'pk001'"), Top: to.Int32Ptr(25), Select: to.StringPtr("PartitionKey,RowKey,Value,Price")} -// pager := client.List(options) // Pass in 'nil' if you want to return all Entities for an account. -// for pager.NextPage(ctx) { -// resp = pager.PageResponse() -// fmt.Printf("The page contains %i results.\n", len(resp.TableEntityQueryResponse.Value)) -// } -// err := pager.Err() -func (t *TableClient) List(queryOptions *ListOptions) TableEntityQueryResponsePager { +// +// options := &ListOptions{Filter: to.StringPtr("PartitionKey eq 'pk001'"), Top: to.Int32Ptr(25), Select: to.StringPtr("PartitionKey,RowKey,Value,Price")} +// pager := client.List(options) // Pass in 'nil' if you want to return all Entities for an account. +// for pager.NextPage(ctx) { +// resp = pager.PageResponse() +// fmt.Printf("The page contains %i results.\n", len(resp.TableEntityQueryResponse.Value)) +// } +// err := pager.Err() +// handle(err) +func (t *TableClient) List(listOptions *ListOptions) TableEntityListResponsePager { return &tableEntityQueryResponsePager{ tableClient: t, - queryOptions: queryOptions, + queryOptions: listOptions, tableQueryOptions: &TableQueryEntitiesOptions{}} } @@ -138,7 +141,7 @@ func (t *TableClient) UpdateEntity(ctx context.Context, entity []byte, etag *str case ReplaceEntity: return t.client.UpdateEntity(ctx, t.Name, partKey, rowkey, &TableUpdateEntityOptions{IfMatch: &ifMatch, TableEntityProperties: mapEntity}, &QueryOptions{}) } - return nil, errors.New("Invalid EntityUpdateMode") + return nil, errInvalidUpdateMode } // InsertEntity inserts an entity if it does not already exist in the table. If the entity does exist, the entity is @@ -164,7 +167,7 @@ func (t *TableClient) InsertEntity(ctx context.Context, entity []byte, updateMod case ReplaceEntity: return t.client.UpdateEntity(ctx, t.Name, partKey, rowkey, &TableUpdateEntityOptions{TableEntityProperties: mapEntity}, &QueryOptions{}) } - return nil, errors.New("Invalid EntityUpdateMode") + return nil, errInvalidUpdateMode } // GetAccessPolicy retrieves details about any stored access policies specified on the table that may be used with the Shared Access Signature @@ -175,7 +178,7 @@ func (t *TableClient) GetAccessPolicy(ctx context.Context) (SignedIdentifierArra // SetAccessPolicy sets stored access policies for the table that may be used with SharedAccessSignature func (t *TableClient) SetAccessPolicy(ctx context.Context, options *TableSetAccessPolicyOptions) (TableSetAccessPolicyResponse, error) { response, err := t.client.SetAccessPolicy(ctx, t.Name, options) - if len(*&options.TableACL) > 5 { + if len(options.TableACL) > 5 { err = tooManyAccessPoliciesError } return response, err diff --git a/sdk/tables/aztable/table_client_test.go b/sdk/tables/aztable/table_client_test.go index 7ddf6715149a..1de071b483a3 100644 --- a/sdk/tables/aztable/table_client_test.go +++ b/sdk/tables/aztable/table_client_test.go @@ -137,7 +137,7 @@ func TestMergeEntity(t *testing.T) { _, updateErr := client.UpdateEntity(ctx, reMarshalled, nil, MergeEntity) require.Nil(updateErr) - var qResp TableEntityQueryByteResponseResponse + var qResp TableEntityListByteResponseResponse pager := client.List(listOptions) for pager.NextPage(ctx) { qResp = pager.PageResponse() @@ -156,7 +156,7 @@ func TestMergeEntity(t *testing.T) { } } -func TestUpsertEntity(t *testing.T) { +func TestInsertEntity(t *testing.T) { for _, service := range services { t.Run(fmt.Sprintf("%v_%v", t.Name(), service), func(t *testing.T) { client, delete := initClientTest(t, service, true) @@ -193,7 +193,7 @@ func TestUpsertEntity(t *testing.T) { require.Nil(err) // 5. Query for new entity - var qResp TableEntityQueryByteResponseResponse + var qResp TableEntityListByteResponseResponse pager := client.List(list) for pager.NextPage(ctx) { qResp = pager.PageResponse() @@ -232,7 +232,7 @@ func TestQuerySimpleEntity(t *testing.T) { list := &ListOptions{Filter: &filter} expectedCount := 4 - var resp TableEntityQueryByteResponseResponse + var resp TableEntityListByteResponseResponse pager := client.List(list) for pager.NextPage(ctx) { resp = pager.PageResponse() @@ -285,7 +285,7 @@ func TestQueryComplexEntity(t *testing.T) { expectedCount := 4 options := &ListOptions{Filter: &filter} - var resp TableEntityQueryByteResponseResponse + var resp TableEntityListByteResponseResponse pager := client.List(options) for pager.NextPage(ctx) { resp = pager.PageResponse() diff --git a/sdk/tables/aztable/table_pagers.go b/sdk/tables/aztable/table_pagers.go index 4b8b61613265..fb1aa9dffdbc 100644 --- a/sdk/tables/aztable/table_pagers.go +++ b/sdk/tables/aztable/table_pagers.go @@ -9,7 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" ) -// TableEntityQueryResponsePager is a Pager for Table entity query results. +// TableEntityListResponsePager is a Pager for Table entity query results. // // NextPage should be called first. It fetches the next available page of results from the service. // If the fetched page contains results, the return value is true, else false. @@ -24,16 +24,16 @@ import ( // fmt.Printf("The page contains %i results.\n", len(resp.TableEntityQueryResponse.Value)) // } // err := pager.Err() -type TableEntityQueryResponsePager interface { +type TableEntityListResponsePager interface { azcore.Pager // PageResponse returns the current TableQueryResponseResponse. - PageResponse() TableEntityQueryByteResponseResponse + PageResponse() TableEntityListByteResponseResponse } type tableEntityQueryResponsePager struct { tableClient *TableClient - current *TableEntityQueryByteResponseResponse + current *TableEntityListByteResponseResponse tableQueryOptions *TableQueryEntitiesOptions queryOptions *ListOptions err error @@ -68,7 +68,7 @@ func (p *tableEntityQueryResponsePager) NextPage(ctx context.Context) bool { // fmt.Printf("The page contains %i results.\n", len(resp.TableEntityQueryResponse.Value)) // } // err := pager.Err() -func (p *tableEntityQueryResponsePager) PageResponse() TableEntityQueryByteResponseResponse { +func (p *tableEntityQueryResponsePager) PageResponse() TableEntityListByteResponseResponse { return *p.current } @@ -77,7 +77,7 @@ func (p *tableEntityQueryResponsePager) Err() error { return p.err } -// TableQueryResponsePager is a Pager for Table Queries +// TableListResponsePager is a Pager for Table Queries // // NextPage should be called first. It fetches the next available page of results from the service. // If the fetched page contains results, the return value is true, else false. @@ -92,16 +92,16 @@ func (p *tableEntityQueryResponsePager) Err() error { // fmt.Printf("The page contains %i results.\n", len(resp.TableEntityQueryResponse.Value)) // } // err := pager.Err() -type TableQueryResponsePager interface { +type TableListResponsePager interface { azcore.Pager // PageResponse returns the current TableQueryResponseResponse. - PageResponse() TableQueryResponseResponse + PageResponse() TableListResponseResponse } type tableQueryResponsePager struct { client *tableClient - current *TableQueryResponseResponse + current *TableListResponseResponse tableQueryOptions *TableQueryOptions queryOptions *ListOptions err error @@ -116,7 +116,7 @@ func (p *tableQueryResponsePager) NextPage(ctx context.Context) bool { } var resp TableQueryResponseResponse resp, p.err = p.client.Query(ctx, p.tableQueryOptions, p.queryOptions.toQueryOptions()) - p.current = &resp + p.current = listResponseFromQueryResponse(resp) p.tableQueryOptions.NextTableName = resp.XMSContinuationNextTableName return p.err == nil && resp.TableQueryResponse.Value != nil && len(resp.TableQueryResponse.Value) > 0 } @@ -128,7 +128,7 @@ func (p *tableQueryResponsePager) NextPage(ctx context.Context) bool { // resp = pager.PageResponse() // fmt.Printf("The page contains %i results.\n", len(resp.TableEntityQueryResponse.Value)) // } -func (p *tableQueryResponsePager) PageResponse() TableQueryResponseResponse { +func (p *tableQueryResponsePager) PageResponse() TableListResponseResponse { return *p.current } diff --git a/sdk/tables/aztable/table_service_client.go b/sdk/tables/aztable/table_service_client.go index 1f67b9f5c1fa..7f25d84229cb 100644 --- a/sdk/tables/aztable/table_service_client.go +++ b/sdk/tables/aztable/table_service_client.go @@ -6,6 +6,7 @@ package aztable import ( "context" "strings" + "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" ) @@ -29,12 +30,10 @@ func NewTableServiceClient(serviceURL string, cred azcore.Credential, options *T } conOptions := options.getConnectionOptions() if isCosmosEndpoint(serviceURL) { - conOptions.PerCallPolicies = []azcore.Policy{CosmosPatchTransformPolicy{}} + conOptions.PerCallPolicies = []azcore.Policy{cosmosPatchTransformPolicy{}} } conOptions.PerCallPolicies = append(conOptions.PerCallPolicies, cred.AuthenticationPolicy(azcore.AuthenticationPolicyOptions{Options: azcore.TokenRequestOptions{Scopes: options.Scopes}})) - for _, p := range options.PerCallOptions { - conOptions.PerCallPolicies = append(conOptions.PerCallPolicies, p) - } + conOptions.PerCallPolicies = append(conOptions.PerCallPolicies, options.PerCallOptions...) con := newConnection(serviceURL, conOptions) return &TableServiceClient{client: &tableClient{con}, service: &serviceClient{con}, cred: cred}, nil } @@ -80,7 +79,7 @@ func (t *TableServiceClient) DeleteTable(ctx context.Context, name string, optio // fmt.Printf("The page contains %i results.\n", len(resp.TableQueryResponse.Value)) // } // err := pager.Err() -func (t *TableServiceClient) ListTables(listOptions *ListOptions) TableQueryResponsePager { +func (t *TableServiceClient) ListTables(listOptions *ListOptions) TableListResponsePager { return &tableQueryResponsePager{ client: t.client, queryOptions: listOptions, @@ -147,6 +146,48 @@ func (t *TableServiceClient) SetProperties(ctx context.Context, properties Table return t.service.SetProperties(ctx, properties, options) } +func (s TableServiceClient) CanGetAccountSASToken() bool { + return s.cred != nil +} + +// GetAccountSASToken is a convenience method for generating a SAS token for the currently pointed at account. +// It can only be used if the supplied azcore.Credential during creation was a SharedKeyCredential. +// This validity can be checked with CanGetAccountSASToken(). +func (t TableServiceClient) GetAccountSASToken(resources AccountSASResourceTypes, permissions AccountSASPermissions, start time.Time, expiry time.Time) (SASQueryParameters, error) { + return AccountSASSignatureValues{ + Version: SASVersion, + Protocol: SASProtocolHTTPS, + Permissions: permissions.String(), + Services: "t", + ResourceTypes: resources.String(), + StartTime: start.UTC(), + ExpiryTime: expiry.UTC(), + }.NewSASQueryParameters(t.cred.(*SharedKeyCredential)) +} + +// GetTableSASToken is a convenience method for generating a SAS token for a specific table. +// It can only be used if the supplied azcore.Credential during creation was a SharedKeyCredential. +// This validity can be checked with CanGetAccountSASToken(). +func (t TableServiceClient) GetTableSASToken(tableName string, permissions TableSASPermissions, start time.Time, expiry time.Time) (SASQueryParameters, error) { + return TableSASSignatureValues{ + TableName: tableName, + Permissions: permissions.String(), + StartTime: start, + ExpiryTime: expiry, + StartPartitionKey: permissions.StartPartitionKey, + StartRowKey: permissions.StartRowKey, + EndPartitionKey: permissions.EndPartitionKey, + EndRowKey: permissions.EndRowKey, + }.NewSASQueryParameters(t.cred.(*SharedKeyCredential)) +} + +// CanGetSASToken returns true if the TableServiceClient was created with a SharedKeyCredential. +// This method can be used to determine if a TableServiceClient is capable of creating a Table SAS or Account SAS +func (t TableServiceClient) CanGetSASToken() bool { + _, ok := t.cred.(*SharedKeyCredential) + return ok +} + func isCosmosEndpoint(url string) bool { isCosmosEmulator := strings.Contains(url, "localhost") && strings.Contains(url, "8902") return isCosmosEmulator || strings.Contains(url, CosmosTableDomain) || strings.Contains(url, LegacyCosmosTableDomain) diff --git a/sdk/tables/aztable/table_service_client_test.go b/sdk/tables/aztable/table_service_client_test.go index 8fc0e16a587c..b6d0c2d435ce 100644 --- a/sdk/tables/aztable/table_service_client_test.go +++ b/sdk/tables/aztable/table_service_client_test.go @@ -94,7 +94,7 @@ func TestQueryTable(t *testing.T) { resultCount := 0 for pager.NextPage(ctx) { resp := pager.PageResponse() - resultCount += len(resp.TableQueryResponse.Value) + resultCount += len(resp.TableListResponse.Value) } require.NoError(t, pager.Err()) @@ -108,7 +108,7 @@ func TestQueryTable(t *testing.T) { pageCount := 0 for pager.NextPage(ctx) { resp := pager.PageResponse() - resultCount += len(resp.TableQueryResponse.Value) + resultCount += len(resp.TableListResponse.Value) pageCount++ } @@ -140,7 +140,7 @@ func TestListTables(t *testing.T) { pager := service.ListTables(nil) for pager.NextPage(ctx) { resp := pager.PageResponse() - count += len(resp.TableQueryResponse.Value) + count += len(resp.TableListResponse.Value) } require.NoError(t, pager.Err()) @@ -167,7 +167,6 @@ func TestGetStatistics(t *testing.T) { require.NoError(t, err) accountName := recording.GetEnvVariable(t, "TABLES_STORAGE_ACCOUNT_NAME", "fakestorageaccount") serviceURL := storageURI(accountName+"-secondary", "core.windows.net") - fmt.Println(serviceURL) service, err := createTableServiceClientForRecording(t, serviceURL, cred) require.NoError(t, err) diff --git a/sdk/tables/aztable/table_transactional_batch.go b/sdk/tables/aztable/table_transactional_batch.go index 6289483df207..d4e7b6ff5d11 100644 --- a/sdk/tables/aztable/table_transactional_batch.go +++ b/sdk/tables/aztable/table_transactional_batch.go @@ -31,8 +31,8 @@ const ( UpdateMerge TableTransactionActionType = "updatemerge" UpdateReplace TableTransactionActionType = "updatereplace" Delete TableTransactionActionType = "delete" - UpsertMerge TableTransactionActionType = "upsertmerge" - UpsertReplace TableTransactionActionType = "upsertreplace" + InsertMerge TableTransactionActionType = "insertmerge" + InsertReplace TableTransactionActionType = "insertreplace" ) const ( @@ -324,7 +324,7 @@ func (t *TableClient) generateEntitySubset(transactionAction *TableTransactionAc } case UpdateMerge: fallthrough - case UpsertMerge: + case InsertMerge: opts := &TableMergeEntityOptions{TableEntityProperties: entity} if len(transactionAction.ETag) > 0 { opts.IfMatch = &transactionAction.ETag @@ -338,7 +338,7 @@ func (t *TableClient) generateEntitySubset(transactionAction *TableTransactionAc } case UpdateReplace: fallthrough - case UpsertReplace: + case InsertReplace: req, err = t.client.updateEntityCreateRequest(ctx, t.Name, entity[partitionKey].(string), entity[rowKey].(string), &TableUpdateEntityOptions{TableEntityProperties: entity, IfMatch: &transactionAction.ETag}, qo) if err != nil { return err diff --git a/sdk/tables/aztable/zc_table_constants.go b/sdk/tables/aztable/zc_table_constants.go index d7d04897ecde..476cd2ba81e0 100644 --- a/sdk/tables/aztable/zc_table_constants.go +++ b/sdk/tables/aztable/zc_table_constants.go @@ -5,7 +5,6 @@ package aztable import "errors" -//nolint const ( timestamp = "Timestamp" partitionKey = "PartitionKey" diff --git a/sdk/tables/aztable/zt_table_recorded_tests.go b/sdk/tables/aztable/zt_table_recorded_tests.go index 259cdea96307..c6ea9f6a4c10 100644 --- a/sdk/tables/aztable/zt_table_recorded_tests.go +++ b/sdk/tables/aztable/zt_table_recorded_tests.go @@ -35,8 +35,8 @@ const ( type EndpointType string const ( - StorageEndpoint EndpointType = "storage" - CosmosEndpoint EndpointType = "cosmos" + storageEndpoint EndpointType = "storage" + cosmosEndpoint EndpointType = "cosmos" ) var ctx = context.Background() @@ -74,7 +74,7 @@ func cleanupTables(context *testContext, tables *[]string) { if tables == nil { pager := c.ListTables(nil) for pager.NextPage(ctx) { - for _, t := range pager.PageResponse().TableQueryResponse.Value { + for _, t := range pager.PageResponse().TableListResponse.Value { _, err := c.DeleteTable(ctx, *t.TableName, nil) if err != nil { fmt.Printf("Error cleaning up tables. %v\n", err.Error())