Skip to content

Commit 22fff23

Browse files
authored
Merge pull request #106 from tombuildsstuff/bugfix/table-id-parsing
bugfix: more safety when parsing table IDs and table entity IDs, also accept table IDs in legacy and newer formats
2 parents 9c02373 + 9e9387a commit 22fff23

File tree

11 files changed

+116
-38
lines changed

11 files changed

+116
-38
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ toolchain go1.21.3
77
require (
88
github.com/google/uuid v1.4.0
99
github.com/hashicorp/go-azure-helpers v0.66.2
10-
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240223.1153421
11-
github.com/hashicorp/go-azure-sdk/sdk v0.20240223.1153421
10+
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240227.1172434
11+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1172434
1212
github.com/stretchr/testify v1.8.4
1313
)
1414

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,20 @@ github.com/hashicorp/go-azure-sdk/resource-manager v0.20240125.1111756 h1:foZtDG
3535
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240125.1111756/go.mod h1:yWRLLjG7PVThIY1NLeLNhp0VtTewOCFrIVIZl/LgPBc=
3636
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240223.1153421 h1:RrPR6RGkkvfWoP+UkmzWNQwoIVsFO/Oj+8lQqQw3880=
3737
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240223.1153421/go.mod h1:gm/5ZkCKtVTV1sSHyMql5a8tB0Z5NBXjF0MGB7Q8chg=
38+
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240227.1130345 h1:h+GSWVxpVzTH0kD+SyRskBsJuKBVqJVCGItMmLsO50I=
39+
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240227.1130345/go.mod h1:G2gxM62VIehZbxVxxmkOSQxngB/ecH9QDO+sgDSTnfo=
40+
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240227.1172434 h1:UNp65kdO4noJDrEe99Od5NfXSnjrrTs0dEyw2MM8jK0=
41+
github.com/hashicorp/go-azure-sdk/resource-manager v0.20240227.1172434/go.mod h1:8Pmp8Bg+FDUjkbuuiLLqysKrKUu5dOIf1dX3KFKNSU8=
3842
github.com/hashicorp/go-azure-sdk/sdk v0.20240219.1162257-0.20240220115734-eeb1a5d96f9a h1:9Qg8M1Yp3WGmJw0FyAeg/4VXmp0m+6kfSJ//etdAfLk=
3943
github.com/hashicorp/go-azure-sdk/sdk v0.20240219.1162257-0.20240220115734-eeb1a5d96f9a/go.mod h1:IKIPyL+hfFWBHABKT0NOWlIEzlusiUBG0SxIfaiv278=
4044
github.com/hashicorp/go-azure-sdk/sdk v0.20240223.1153421 h1:unXuyut6yDlY9kaoworHj2f1gd/c+mSf8qh3WkMyB1Q=
4145
github.com/hashicorp/go-azure-sdk/sdk v0.20240223.1153421/go.mod h1:IKIPyL+hfFWBHABKT0NOWlIEzlusiUBG0SxIfaiv278=
46+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1130345 h1:iIfqojPBH9SskoKqcA1bZGbGQcEWyjx+2+0PHwQafgc=
47+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1130345/go.mod h1:IKIPyL+hfFWBHABKT0NOWlIEzlusiUBG0SxIfaiv278=
48+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1172434 h1:IX9e6DRUYl+1skjZPpfdhnEV3cx8dNX1qECYSVcD288=
49+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1172434/go.mod h1:IKIPyL+hfFWBHABKT0NOWlIEzlusiUBG0SxIfaiv278=
50+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1200200 h1:o0D7vswmP3fnm9slJ8YcsQJq7QogeMAjq5yl3a+kbDo=
51+
github.com/hashicorp/go-azure-sdk/sdk v0.20240227.1200200/go.mod h1:IKIPyL+hfFWBHABKT0NOWlIEzlusiUBG0SxIfaiv278=
4252
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
4353
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
4454
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=

storage/2020-08-04/blob/accounts/get_service_properties.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (c Client) GetServiceProperties(ctx context.Context, accountName string) (r
1919
}
2020

2121
opts := client.RequestOptions{
22-
ContentType: "text/xml",
22+
ContentType: "application/xml",
2323
ExpectedStatusCodes: []int{
2424
http.StatusOK,
2525
},

storage/2020-08-04/blob/accounts/set_service_properties.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func (c Client) SetServiceProperties(ctx context.Context, accountName string, in
1818
}
1919

2020
opts := client.RequestOptions{
21-
ContentType: "text/xml",
21+
ContentType: "application/xml",
2222
ExpectedStatusCodes: []int{
2323
http.StatusAccepted,
2424
},

storage/2020-08-04/table/entities/resource_id.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (b EntityId) String() string {
5252

5353
// ParseEntityID parses `input` into a Entity ID using a known `domainSuffix`
5454
func ParseEntityID(input, domainSuffix string) (*EntityId, error) {
55-
// example: https://foo.table.core.windows.net/Bar1(PartitionKey='partition1',RowKey='row1')
55+
// example: https://foo.table.core.windows.net/bar(PartitionKey='partition1',RowKey='row1')
5656
if input == "" {
5757
return nil, fmt.Errorf("`input` was empty")
5858
}
@@ -79,23 +79,28 @@ func ParseEntityID(input, domainSuffix string) (*EntityId, error) {
7979

8080
// Tables and Table Entities are similar with table being `table1` and entities
8181
// being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table
82-
key := strings.TrimPrefix(uri.Path, "/")
83-
if !strings.Contains(key, "(") || !strings.HasSuffix(key, ")") {
84-
return nil, fmt.Errorf("expected the path to be an entity name but got a table name %q", key)
82+
slug := strings.TrimPrefix(uri.Path, "/")
83+
if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") {
84+
// Ensure we do not parse a Table ID in the format: https://foo.table.core.windows.net/Table('foo')
85+
return nil, fmt.Errorf("expected the path to be an entity name but got a table name: %q", slug)
86+
} else if !strings.Contains(slug, "(") || !strings.HasSuffix(slug, ")") {
87+
// Ensure we do not try to parse a bare table name
88+
return nil, fmt.Errorf("expected the path to be an entity name but got an invalid format, possibly a table name: %q", slug)
8589
}
8690

87-
indexOfFirstBracket := strings.Index(key, "(")
88-
tableName := key[0:indexOfFirstBracket]
89-
componentString := key[indexOfFirstBracket:]
91+
indexOfFirstBracket := strings.Index(slug, "(")
92+
tableName := slug[0:indexOfFirstBracket]
93+
componentString := slug[indexOfFirstBracket:]
9094
componentString = strings.TrimPrefix(componentString, "(")
9195
componentString = strings.TrimSuffix(componentString, ")")
9296
components := strings.Split(componentString, ",")
9397
if len(components) != 2 {
94-
return nil, fmt.Errorf("expected the path to be an entity name but got %q", key)
98+
return nil, fmt.Errorf("expected the path to be an entity name but got %q", slug)
9599
}
96100

97101
partitionKey := parseValueFromKey(components[0], "PartitionKey")
98102
rowKey := parseValueFromKey(components[1], "RowKey")
103+
99104
return &EntityId{
100105
AccountId: *account,
101106
TableName: tableName,

storage/2020-08-04/table/tables/resource_id.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func (b TableId) String() string {
4848

4949
// ParseTableID parses `input` into a Table ID using a known `domainSuffix`
5050
func ParseTableID(input, domainSuffix string) (*TableId, error) {
51-
// example: https://foo.table.core.windows.net/Bar
51+
// example: https://foo.table.core.windows.net/Table('bar')
5252
if input == "" {
5353
return nil, fmt.Errorf("`input` was empty")
5454
}
@@ -73,12 +73,27 @@ func ParseTableID(input, domainSuffix string) (*TableId, error) {
7373
return nil, fmt.Errorf("expected the path to contain 1 segment but got %d", len(segments))
7474
}
7575

76-
// Tables and Table Entities are similar with table being `table1` and entities
77-
// being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table
78-
tableName := strings.TrimPrefix(uri.Path, "/")
79-
if strings.Contains(tableName, "(") || strings.Contains(tableName, ")") {
76+
// Tables and Table Entities are similar however Tables use a reserved namespace, for example:
77+
// Table('tableName')
78+
// whereas Entities begin with the actual table name, for example:
79+
// tableName(PartitionKey='samplepartition',RowKey='samplerow')
80+
// However, there was a period of time when Table IDs did not use the reserved namespace, so we attempt to parse
81+
// both forms for maximum compatibility.
82+
var tableName string
83+
slug := strings.TrimPrefix(uri.Path, "/")
84+
if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") {
85+
// Ensure both prefix and suffix are present before trimming them out
86+
tableName = strings.TrimSuffix(strings.TrimPrefix(slug, "Tables('"), "')")
87+
} else if !strings.Contains(slug, "(") && !strings.HasSuffix(slug, ")") {
88+
// Also accept a bare table name
89+
tableName = slug
90+
} else {
8091
return nil, fmt.Errorf("expected the path to a table name and not an entity name but got %q", tableName)
8192
}
93+
if tableName == "" {
94+
return nil, fmt.Errorf("expected the path to a table name but the path was empty")
95+
}
96+
8297
return &TableId{
8398
AccountId: *account,
8499
TableName: tableName,

storage/2023-11-03/blob/accounts/get_service_properties.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func (c Client) GetServiceProperties(ctx context.Context, accountName string) (r
1919
}
2020

2121
opts := client.RequestOptions{
22-
ContentType: "text/xml",
22+
ContentType: "application/xml; charset=utf-8",
2323
ExpectedStatusCodes: []int{
2424
http.StatusOK,
2525
},

storage/2023-11-03/blob/accounts/set_service_properties.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func (c Client) SetServiceProperties(ctx context.Context, accountName string, in
1818
}
1919

2020
opts := client.RequestOptions{
21-
ContentType: "text/xml",
21+
ContentType: "application/xml; charset=utf-8",
2222
ExpectedStatusCodes: []int{
2323
http.StatusAccepted,
2424
},

storage/2023-11-03/table/entities/resource_id.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (b EntityId) String() string {
5252

5353
// ParseEntityID parses `input` into a Entity ID using a known `domainSuffix`
5454
func ParseEntityID(input, domainSuffix string) (*EntityId, error) {
55-
// example: https://foo.table.core.windows.net/Bar1(PartitionKey='partition1',RowKey='row1')
55+
// example: https://foo.table.core.windows.net/bar(PartitionKey='partition1',RowKey='row1')
5656
if input == "" {
5757
return nil, fmt.Errorf("`input` was empty")
5858
}
@@ -79,23 +79,28 @@ func ParseEntityID(input, domainSuffix string) (*EntityId, error) {
7979

8080
// Tables and Table Entities are similar with table being `table1` and entities
8181
// being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table
82-
key := strings.TrimPrefix(uri.Path, "/")
83-
if !strings.Contains(key, "(") || !strings.HasSuffix(key, ")") {
84-
return nil, fmt.Errorf("expected the path to be an entity name but got a table name %q", key)
82+
slug := strings.TrimPrefix(uri.Path, "/")
83+
if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") {
84+
// Ensure we do not parse a Table ID in the format: https://foo.table.core.windows.net/Table('foo')
85+
return nil, fmt.Errorf("expected the path to be an entity name but got a table name: %q", slug)
86+
} else if !strings.Contains(slug, "(") || !strings.HasSuffix(slug, ")") {
87+
// Ensure we do not try to parse a bare table name
88+
return nil, fmt.Errorf("expected the path to be an entity name but got an invalid format, possibly a table name: %q", slug)
8589
}
8690

87-
indexOfFirstBracket := strings.Index(key, "(")
88-
tableName := key[0:indexOfFirstBracket]
89-
componentString := key[indexOfFirstBracket:]
91+
indexOfFirstBracket := strings.Index(slug, "(")
92+
tableName := slug[0:indexOfFirstBracket]
93+
componentString := slug[indexOfFirstBracket:]
9094
componentString = strings.TrimPrefix(componentString, "(")
9195
componentString = strings.TrimSuffix(componentString, ")")
9296
components := strings.Split(componentString, ",")
9397
if len(components) != 2 {
94-
return nil, fmt.Errorf("expected the path to be an entity name but got %q", key)
98+
return nil, fmt.Errorf("expected the path to be an entity name but got %q", slug)
9599
}
96100

97101
partitionKey := parseValueFromKey(components[0], "PartitionKey")
98102
rowKey := parseValueFromKey(components[1], "RowKey")
103+
99104
return &EntityId{
100105
AccountId: *account,
101106
TableName: tableName,

storage/2023-11-03/table/tables/resource_id.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func NewTableID(accountId accounts.AccountId, tableName string) TableId {
3636
}
3737

3838
func (b TableId) ID() string {
39-
return fmt.Sprintf("%s/%s", b.AccountId.ID(), b.TableName)
39+
return fmt.Sprintf("%s/Tables('%s')", b.AccountId.ID(), b.TableName)
4040
}
4141

4242
func (b TableId) String() string {
@@ -48,7 +48,7 @@ func (b TableId) String() string {
4848

4949
// ParseTableID parses `input` into a Table ID using a known `domainSuffix`
5050
func ParseTableID(input, domainSuffix string) (*TableId, error) {
51-
// example: https://foo.table.core.windows.net/Bar
51+
// example: https://foo.table.core.windows.net/Table('bar')
5252
if input == "" {
5353
return nil, fmt.Errorf("`input` was empty")
5454
}
@@ -73,12 +73,27 @@ func ParseTableID(input, domainSuffix string) (*TableId, error) {
7373
return nil, fmt.Errorf("expected the path to contain 1 segment but got %d", len(segments))
7474
}
7575

76-
// Tables and Table Entities are similar with table being `table1` and entities
77-
// being `table1(PartitionKey='samplepartition',RowKey='samplerow')` so we need to validate this is a table
78-
tableName := strings.TrimPrefix(uri.Path, "/")
79-
if strings.Contains(tableName, "(") || strings.Contains(tableName, ")") {
76+
// Tables and Table Entities are similar however Tables use a reserved namespace, for example:
77+
// Table('tableName')
78+
// whereas Entities begin with the actual table name, for example:
79+
// tableName(PartitionKey='samplepartition',RowKey='samplerow')
80+
// However, there was a period of time when Table IDs did not use the reserved namespace, so we attempt to parse
81+
// both forms for maximum compatibility.
82+
var tableName string
83+
slug := strings.TrimPrefix(uri.Path, "/")
84+
if strings.HasPrefix(slug, "Tables('") && strings.HasSuffix(slug, "')") {
85+
// Ensure both prefix and suffix are present before trimming them out
86+
tableName = strings.TrimSuffix(strings.TrimPrefix(slug, "Tables('"), "')")
87+
} else if !strings.Contains(slug, "(") && !strings.HasSuffix(slug, ")") {
88+
// Also accept a bare table name
89+
tableName = slug
90+
} else {
8091
return nil, fmt.Errorf("expected the path to a table name and not an entity name but got %q", tableName)
8192
}
93+
if tableName == "" {
94+
return nil, fmt.Errorf("expected the path to a table name but the path was empty")
95+
}
96+
8297
return &TableId{
8398
AccountId: *account,
8499
TableName: tableName,

storage/2023-11-03/table/tables/resource_id_test.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,34 @@ func TestGetResourceManagerResourceID(t *testing.T) {
1616
}
1717

1818
func TestParseTableIDStandard(t *testing.T) {
19+
input := "https://example1.table.core.windows.net/Tables('table1')"
20+
expected := TableId{
21+
AccountId: accounts.AccountId{
22+
AccountName: "example1",
23+
SubDomainType: accounts.TableSubDomainType,
24+
DomainSuffix: "core.windows.net",
25+
},
26+
TableName: "table1",
27+
}
28+
actual, err := ParseTableID(input, "core.windows.net")
29+
if err != nil {
30+
t.Fatalf(err.Error())
31+
}
32+
if actual.AccountId.AccountName != expected.AccountId.AccountName {
33+
t.Fatalf("expected AccountName to be %q but got %q", expected.AccountId.AccountName, actual.AccountId.AccountName)
34+
}
35+
if actual.AccountId.SubDomainType != expected.AccountId.SubDomainType {
36+
t.Fatalf("expected SubDomainType to be %q but got %q", expected.AccountId.SubDomainType, actual.AccountId.SubDomainType)
37+
}
38+
if actual.AccountId.DomainSuffix != expected.AccountId.DomainSuffix {
39+
t.Fatalf("expected DomainSuffix to be %q but got %q", expected.AccountId.DomainSuffix, actual.AccountId.DomainSuffix)
40+
}
41+
if actual.TableName != expected.TableName {
42+
t.Fatalf("expected TableName to be %q but got %q", expected.TableName, actual.TableName)
43+
}
44+
}
45+
46+
func TestParseTableIDLegacy(t *testing.T) {
1947
input := "https://example1.table.core.windows.net/table1"
2048
expected := TableId{
2149
AccountId: accounts.AccountId{
@@ -44,7 +72,7 @@ func TestParseTableIDStandard(t *testing.T) {
4472
}
4573

4674
func TestParseTableIDInADNSZone(t *testing.T) {
47-
input := "https://example1.zone1.table.storage.azure.net/table1"
75+
input := "https://example1.zone1.table.storage.azure.net/Tables('table1')"
4876
expected := TableId{
4977
AccountId: accounts.AccountId{
5078
AccountName: "example1",
@@ -76,7 +104,7 @@ func TestParseTableIDInADNSZone(t *testing.T) {
76104
}
77105

78106
func TestParseTableIDInAnEdgeZone(t *testing.T) {
79-
input := "https://example1.table.zone1.edgestorage.azure.net/table1"
107+
input := "https://example1.table.zone1.edgestorage.azure.net/Tables('table1')"
80108
expected := TableId{
81109
AccountId: accounts.AccountId{
82110
AccountName: "example1",
@@ -121,7 +149,7 @@ func TestFormatTableIDStandard(t *testing.T) {
121149
},
122150
TableName: "table1",
123151
}.ID()
124-
expected := "https://example1.table.core.windows.net/table1"
152+
expected := "https://example1.table.core.windows.net/Tables('table1')"
125153
if actual != expected {
126154
t.Fatalf("expected %q but got %q", expected, actual)
127155
}
@@ -138,7 +166,7 @@ func TestFormatTableIDInDNSZone(t *testing.T) {
138166
},
139167
TableName: "table1",
140168
}.ID()
141-
expected := "https://example1.zone2.table.storage.azure.net/table1"
169+
expected := "https://example1.zone2.table.storage.azure.net/Tables('table1')"
142170
if actual != expected {
143171
t.Fatalf("expected %q but got %q", expected, actual)
144172
}
@@ -155,7 +183,7 @@ func TestFormatTableIDInEdgeZone(t *testing.T) {
155183
},
156184
TableName: "table1",
157185
}.ID()
158-
expected := "https://example1.table.zone2.edgestorage.azure.net/table1"
186+
expected := "https://example1.table.zone2.edgestorage.azure.net/Tables('table1')"
159187
if actual != expected {
160188
t.Fatalf("expected %q but got %q", expected, actual)
161189
}

0 commit comments

Comments
 (0)