From 7ffcb6e91c8900fed6fd616fae943f67f4d50825 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Mon, 26 Feb 2024 15:56:39 +0000 Subject: [PATCH] bugfix: more safety when parsing table IDs and table entity IDs, align ID string with AzureRM provider, and also accept table IDs in both legacy and newer formats --- .../2020-08-04/table/entities/resource_id.go | 21 +++++++++------ .../2020-08-04/table/tables/resource_id.go | 25 +++++++++++++---- .../2023-11-03/table/entities/resource_id.go | 21 +++++++++------ .../2023-11-03/table/tables/resource_id.go | 27 ++++++++++++++----- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/storage/2020-08-04/table/entities/resource_id.go b/storage/2020-08-04/table/entities/resource_id.go index 4e72402..04ebe79 100644 --- a/storage/2020-08-04/table/entities/resource_id.go +++ b/storage/2020-08-04/table/entities/resource_id.go @@ -52,7 +52,7 @@ func (b EntityId) String() string { // ParseEntityID parses `input` into a Entity ID using a known `domainSuffix` func ParseEntityID(input, domainSuffix string) (*EntityId, error) { - // example: https://foo.table.core.windows.net/Bar1(PartitionKey='partition1',RowKey='row1') + // example: https://foo.table.core.windows.net/bar(PartitionKey='partition1',RowKey='row1') if input == "" { return nil, fmt.Errorf("`input` was empty") } @@ -79,23 +79,28 @@ func ParseEntityID(input, domainSuffix string) (*EntityId, error) { // Tables and Table Entities are similar with table being `table1` and entities // being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table - key := strings.TrimPrefix(uri.Path, "/") - if !strings.Contains(key, "(") || !strings.HasSuffix(key, ")") { - return nil, fmt.Errorf("expected the path to be an entity name but got a table name %q", key) + slug := strings.TrimPrefix(uri.Path, "/") + if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") { + // Ensure we do not parse a Table ID in the format: https://foo.table.core.windows.net/Table('foo') + return nil, fmt.Errorf("expected the path to be an entity name but got a table name: %q", slug) + } else if !strings.Contains(slug, "(") || !strings.HasSuffix(slug, ")") { + // Ensure we do not try to parse a bare table name + return nil, fmt.Errorf("expected the path to be an entity name but got an invalid format, possibly a table name: %q", slug) } - indexOfFirstBracket := strings.Index(key, "(") - tableName := key[0:indexOfFirstBracket] - componentString := key[indexOfFirstBracket:] + indexOfFirstBracket := strings.Index(slug, "(") + tableName := slug[0:indexOfFirstBracket] + componentString := slug[indexOfFirstBracket:] componentString = strings.TrimPrefix(componentString, "(") componentString = strings.TrimSuffix(componentString, ")") components := strings.Split(componentString, ",") if len(components) != 2 { - return nil, fmt.Errorf("expected the path to be an entity name but got %q", key) + return nil, fmt.Errorf("expected the path to be an entity name but got %q", slug) } partitionKey := parseValueFromKey(components[0], "PartitionKey") rowKey := parseValueFromKey(components[1], "RowKey") + return &EntityId{ AccountId: *account, TableName: tableName, diff --git a/storage/2020-08-04/table/tables/resource_id.go b/storage/2020-08-04/table/tables/resource_id.go index 2dbc4c9..37d0205 100644 --- a/storage/2020-08-04/table/tables/resource_id.go +++ b/storage/2020-08-04/table/tables/resource_id.go @@ -48,7 +48,7 @@ func (b TableId) String() string { // ParseTableID parses `input` into a Table ID using a known `domainSuffix` func ParseTableID(input, domainSuffix string) (*TableId, error) { - // example: https://foo.table.core.windows.net/Bar + // example: https://foo.table.core.windows.net/Table('bar') if input == "" { return nil, fmt.Errorf("`input` was empty") } @@ -73,12 +73,27 @@ func ParseTableID(input, domainSuffix string) (*TableId, error) { return nil, fmt.Errorf("expected the path to contain 1 segment but got %d", len(segments)) } - // Tables and Table Entities are similar with table being `table1` and entities - // being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table - tableName := strings.TrimPrefix(uri.Path, "/") - if strings.Contains(tableName, "(") || strings.Contains(tableName, ")") { + // Tables and Table Entities are similar however Tables use a reserved namespace, for example: + // Table('tableName') + // whereas Entities begin with the actual table name, for example: + // tableName(PartitionKey='samplepartition',RowKey='samplerow') + // However, there was a period of time when Table IDs did not use the reserved namespace, so we attempt to parse + // both forms for maximum compatibility. + var tableName string + slug := strings.TrimPrefix(uri.Path, "/") + if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") { + // Ensure both prefix and suffix are present before trimming them out + tableName = strings.TrimSuffix(strings.TrimPrefix(slug, "Tables('"), "')") + } else if !strings.Contains(slug, "(") && !strings.HasSuffix(slug, ")") { + // Also accept a bare table name + tableName = slug + } else { return nil, fmt.Errorf("expected the path to a table name and not an entity name but got %q", tableName) } + if tableName == "" { + return nil, fmt.Errorf("expected the path to a table name but the path was empty") + } + return &TableId{ AccountId: *account, TableName: tableName, diff --git a/storage/2023-11-03/table/entities/resource_id.go b/storage/2023-11-03/table/entities/resource_id.go index 0403b5e..741c24b 100644 --- a/storage/2023-11-03/table/entities/resource_id.go +++ b/storage/2023-11-03/table/entities/resource_id.go @@ -52,7 +52,7 @@ func (b EntityId) String() string { // ParseEntityID parses `input` into a Entity ID using a known `domainSuffix` func ParseEntityID(input, domainSuffix string) (*EntityId, error) { - // example: https://foo.table.core.windows.net/Bar1(PartitionKey='partition1',RowKey='row1') + // example: https://foo.table.core.windows.net/bar(PartitionKey='partition1',RowKey='row1') if input == "" { return nil, fmt.Errorf("`input` was empty") } @@ -79,23 +79,28 @@ func ParseEntityID(input, domainSuffix string) (*EntityId, error) { // Tables and Table Entities are similar with table being `table1` and entities // being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table - key := strings.TrimPrefix(uri.Path, "/") - if !strings.Contains(key, "(") || !strings.HasSuffix(key, ")") { - return nil, fmt.Errorf("expected the path to be an entity name but got a table name %q", key) + slug := strings.TrimPrefix(uri.Path, "/") + if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") { + // Ensure we do not parse a Table ID in the format: https://foo.table.core.windows.net/Table('foo') + return nil, fmt.Errorf("expected the path to be an entity name but got a table name: %q", slug) + } else if !strings.Contains(slug, "(") || !strings.HasSuffix(slug, ")") { + // Ensure we do not try to parse a bare table name + return nil, fmt.Errorf("expected the path to be an entity name but got an invalid format, possibly a table name: %q", slug) } - indexOfFirstBracket := strings.Index(key, "(") - tableName := key[0:indexOfFirstBracket] - componentString := key[indexOfFirstBracket:] + indexOfFirstBracket := strings.Index(slug, "(") + tableName := slug[0:indexOfFirstBracket] + componentString := slug[indexOfFirstBracket:] componentString = strings.TrimPrefix(componentString, "(") componentString = strings.TrimSuffix(componentString, ")") components := strings.Split(componentString, ",") if len(components) != 2 { - return nil, fmt.Errorf("expected the path to be an entity name but got %q", key) + return nil, fmt.Errorf("expected the path to be an entity name but got %q", slug) } partitionKey := parseValueFromKey(components[0], "PartitionKey") rowKey := parseValueFromKey(components[1], "RowKey") + return &EntityId{ AccountId: *account, TableName: tableName, diff --git a/storage/2023-11-03/table/tables/resource_id.go b/storage/2023-11-03/table/tables/resource_id.go index 8c5072f..2dcf79f 100644 --- a/storage/2023-11-03/table/tables/resource_id.go +++ b/storage/2023-11-03/table/tables/resource_id.go @@ -36,7 +36,7 @@ func NewTableID(accountId accounts.AccountId, tableName string) TableId { } func (b TableId) ID() string { - return fmt.Sprintf("%s/%s", b.AccountId.ID(), b.TableName) + return fmt.Sprintf("%s/Tables('%s')", b.AccountId.ID(), b.TableName) } func (b TableId) String() string { @@ -48,7 +48,7 @@ func (b TableId) String() string { // ParseTableID parses `input` into a Table ID using a known `domainSuffix` func ParseTableID(input, domainSuffix string) (*TableId, error) { - // example: https://foo.table.core.windows.net/Bar + // example: https://foo.table.core.windows.net/Table('bar') if input == "" { return nil, fmt.Errorf("`input` was empty") } @@ -73,12 +73,27 @@ func ParseTableID(input, domainSuffix string) (*TableId, error) { return nil, fmt.Errorf("expected the path to contain 1 segment but got %d", len(segments)) } - // Tables and Table Entities are similar with table being `table1` and entities - // being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table - tableName := strings.TrimPrefix(uri.Path, "/") - if strings.Contains(tableName, "(") || strings.Contains(tableName, ")") { + // Tables and Table Entities are similar however Tables use a reserved namespace, for example: + // Table('tableName') + // whereas Entities begin with the actual table name, for example: + // tableName(PartitionKey='samplepartition',RowKey='samplerow') + // However, there was a period of time when Table IDs did not use the reserved namespace, so we attempt to parse + // both forms for maximum compatibility. + var tableName string + slug := strings.TrimPrefix(uri.Path, "/") + if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") { + // Ensure both prefix and suffix are present before trimming them out + tableName = strings.TrimSuffix(strings.TrimPrefix(slug, "Tables('"), "')") + } else if !strings.Contains(slug, "(") && !strings.HasSuffix(slug, ")") { + // Also accept a bare table name + tableName = slug + } else { return nil, fmt.Errorf("expected the path to a table name and not an entity name but got %q", tableName) } + if tableName == "" { + return nil, fmt.Errorf("expected the path to a table name but the path was empty") + } + return &TableId{ AccountId: *account, TableName: tableName,