Skip to content

Commit 3d9be97

Browse files
pierreprinettiEmilienM
authored andcommitted
objectstorage: Reject container names with a slash
As per the [OpenStack object-storage docs](https://docs.openstack.org/api-ref/object-store/#create-container), container names must not contain a slash (`/`). With this patch, objectstorage functions error when called with a containerName containing a slash.
1 parent 7e05edf commit 3d9be97

File tree

11 files changed

+427
-47
lines changed

11 files changed

+427
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package containers
2+
3+
import "github.com/gophercloud/gophercloud"
4+
5+
// ErrInvalidContainerName signals a container name containing an illegal
6+
// character.
7+
type ErrInvalidContainerName struct {
8+
gophercloud.BaseError
9+
}
10+
11+
func (e ErrInvalidContainerName) Error() string {
12+
return "A container name must not contain: " + forbiddenContainerRunes
13+
}

openstack/objectstorage/v1/containers/requests.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ func (opts CreateOpts) ToContainerCreateMap() (map[string]string, error) {
9696

9797
// Create is a function that creates a new container.
9898
func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsBuilder) (r CreateResult) {
99+
url, err := createURL(c, containerName)
100+
if err != nil {
101+
r.Err = err
102+
return
103+
}
99104
h := make(map[string]string)
100105
if opts != nil {
101106
headers, err := opts.ToContainerCreateMap()
@@ -107,7 +112,7 @@ func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsB
107112
h[k] = v
108113
}
109114
}
110-
resp, err := c.Request("PUT", createURL(c, containerName), &gophercloud.RequestOpts{
115+
resp, err := c.Request("PUT", url, &gophercloud.RequestOpts{
111116
MoreHeaders: h,
112117
OkCodes: []int{201, 202, 204},
113118
})
@@ -138,7 +143,12 @@ func BulkDelete(c *gophercloud.ServiceClient, containers []string) (r BulkDelete
138143

139144
// Delete is a function that deletes a container.
140145
func Delete(c *gophercloud.ServiceClient, containerName string) (r DeleteResult) {
141-
resp, err := c.Delete(deleteURL(c, containerName), nil)
146+
url, err := deleteURL(c, containerName)
147+
if err != nil {
148+
r.Err = err
149+
return
150+
}
151+
resp, err := c.Delete(url, nil)
142152
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
143153
return
144154
}
@@ -189,6 +199,11 @@ func (opts UpdateOpts) ToContainerUpdateMap() (map[string]string, error) {
189199
// Update is a function that creates, updates, or deletes a container's
190200
// metadata.
191201
func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsBuilder) (r UpdateResult) {
202+
url, err := updateURL(c, containerName)
203+
if err != nil {
204+
r.Err = err
205+
return
206+
}
192207
h := make(map[string]string)
193208
if opts != nil {
194209
headers, err := opts.ToContainerUpdateMap()
@@ -201,7 +216,7 @@ func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsB
201216
h[k] = v
202217
}
203218
}
204-
resp, err := c.Request("POST", updateURL(c, containerName), &gophercloud.RequestOpts{
219+
resp, err := c.Request("POST", url, &gophercloud.RequestOpts{
205220
MoreHeaders: h,
206221
OkCodes: []int{201, 202, 204},
207222
})
@@ -229,6 +244,11 @@ func (opts GetOpts) ToContainerGetMap() (map[string]string, error) {
229244
// the custom metadata, pass the GetResult response to the ExtractMetadata
230245
// function.
231246
func Get(c *gophercloud.ServiceClient, containerName string, opts GetOptsBuilder) (r GetResult) {
247+
url, err := getURL(c, containerName)
248+
if err != nil {
249+
r.Err = err
250+
return
251+
}
232252
h := make(map[string]string)
233253
if opts != nil {
234254
headers, err := opts.ToContainerGetMap()
@@ -241,7 +261,7 @@ func Get(c *gophercloud.ServiceClient, containerName string, opts GetOptsBuilder
241261
h[k] = v
242262
}
243263
}
244-
resp, err := c.Head(getURL(c, containerName), &gophercloud.RequestOpts{
264+
resp, err := c.Head(url, &gophercloud.RequestOpts{
245265
MoreHeaders: h,
246266
OkCodes: []int{200, 204},
247267
})

openstack/objectstorage/v1/containers/testing/fixtures.go

+39-6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ import (
1010
fake "github.com/gophercloud/gophercloud/testhelper/client"
1111
)
1212

13+
type handlerOptions struct {
14+
path string
15+
}
16+
17+
type option func(*handlerOptions)
18+
19+
func WithPath(s string) option {
20+
return func(h *handlerOptions) {
21+
h.path = s
22+
}
23+
}
24+
1325
// ExpectedListInfo is the result expected from a call to `List` when full
1426
// info is requested.
1527
var ExpectedListInfo = []containers.Container{
@@ -127,8 +139,15 @@ func HandleCreateContainerSuccessfully(t *testing.T) {
127139

128140
// HandleDeleteContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
129141
// responds with a `Delete` response.
130-
func HandleDeleteContainerSuccessfully(t *testing.T) {
131-
th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
142+
func HandleDeleteContainerSuccessfully(t *testing.T, options ...option) {
143+
ho := handlerOptions{
144+
path: "/testContainer",
145+
}
146+
for _, apply := range options {
147+
apply(&ho)
148+
}
149+
150+
th.Mux.HandleFunc(ho.path, func(w http.ResponseWriter, r *http.Request) {
132151
th.TestMethod(t, r, "DELETE")
133152
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
134153
th.TestHeader(t, r, "Accept", "application/json")
@@ -167,8 +186,15 @@ func HandleBulkDeleteSuccessfully(t *testing.T) {
167186

168187
// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
169188
// responds with a `Update` response.
170-
func HandleUpdateContainerSuccessfully(t *testing.T) {
171-
th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
189+
func HandleUpdateContainerSuccessfully(t *testing.T, options ...option) {
190+
ho := handlerOptions{
191+
path: "/testContainer",
192+
}
193+
for _, apply := range options {
194+
apply(&ho)
195+
}
196+
197+
th.Mux.HandleFunc(ho.path, func(w http.ResponseWriter, r *http.Request) {
172198
th.TestMethod(t, r, "POST")
173199
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
174200
th.TestHeader(t, r, "Accept", "application/json")
@@ -183,8 +209,15 @@ func HandleUpdateContainerSuccessfully(t *testing.T) {
183209

184210
// HandleGetContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
185211
// responds with a `Get` response.
186-
func HandleGetContainerSuccessfully(t *testing.T) {
187-
th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
212+
func HandleGetContainerSuccessfully(t *testing.T, options ...option) {
213+
ho := handlerOptions{
214+
path: "/testContainer",
215+
}
216+
for _, apply := range options {
217+
apply(&ho)
218+
}
219+
220+
th.Mux.HandleFunc(ho.path, func(w http.ResponseWriter, r *http.Request) {
188221
th.TestMethod(t, r, "HEAD")
189222
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
190223
th.TestHeader(t, r, "Accept", "application/json")

openstack/objectstorage/v1/containers/testing/requests_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,74 @@ var (
1414
metadata = map[string]string{"gophercloud-test": "containers"}
1515
)
1616

17+
func TestContainerNames(t *testing.T) {
18+
for _, tc := range [...]struct {
19+
name string
20+
containerName string
21+
}{
22+
{
23+
"rejects_a_slash",
24+
"one/two",
25+
},
26+
{
27+
"rejects_an_escaped_slash",
28+
"one%2Ftwo",
29+
},
30+
{
31+
"rejects_an_escaped_slash_lowercase",
32+
"one%2ftwo",
33+
},
34+
} {
35+
t.Run(tc.name, func(t *testing.T) {
36+
t.Run("create", func(t *testing.T) {
37+
th.SetupHTTP()
38+
defer th.TeardownHTTP()
39+
HandleCreateContainerSuccessfully(t)
40+
41+
_, err := containers.Create(fake.ServiceClient(), tc.containerName, nil).Extract()
42+
th.CheckErr(t, err, &containers.ErrInvalidContainerName{})
43+
})
44+
t.Run("delete", func(t *testing.T) {
45+
th.SetupHTTP()
46+
defer th.TeardownHTTP()
47+
HandleDeleteContainerSuccessfully(t, WithPath("/"))
48+
49+
res := containers.Delete(fake.ServiceClient(), tc.containerName)
50+
th.CheckErr(t, res.Err, &containers.ErrInvalidContainerName{})
51+
})
52+
t.Run("update", func(t *testing.T) {
53+
th.SetupHTTP()
54+
defer th.TeardownHTTP()
55+
HandleUpdateContainerSuccessfully(t, WithPath("/"))
56+
57+
contentType := "text/plain"
58+
options := &containers.UpdateOpts{
59+
Metadata: map[string]string{"foo": "bar"},
60+
ContainerWrite: new(string),
61+
ContainerRead: new(string),
62+
ContainerSyncTo: new(string),
63+
ContainerSyncKey: new(string),
64+
ContentType: &contentType,
65+
}
66+
res := containers.Update(fake.ServiceClient(), tc.containerName, options)
67+
th.CheckErr(t, res.Err, &containers.ErrInvalidContainerName{})
68+
})
69+
t.Run("get", func(t *testing.T) {
70+
th.SetupHTTP()
71+
defer th.TeardownHTTP()
72+
HandleGetContainerSuccessfully(t, WithPath("/"))
73+
74+
res := containers.Get(fake.ServiceClient(), tc.containerName, nil)
75+
_, err := res.ExtractMetadata()
76+
th.CheckErr(t, err, &containers.ErrInvalidContainerName{})
77+
78+
_, err = res.Extract()
79+
th.CheckErr(t, err, &containers.ErrInvalidContainerName{})
80+
})
81+
})
82+
}
83+
}
84+
1785
func TestListContainerInfo(t *testing.T) {
1886
th.SetupHTTP()
1987
defer th.TeardownHTTP()

openstack/objectstorage/v1/containers/urls.go

+33-6
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
11
package containers
22

3-
import "github.com/gophercloud/gophercloud"
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/gophercloud/gophercloud"
8+
)
9+
10+
const forbiddenContainerRunes = "/"
11+
12+
func CheckContainerName(s string) error {
13+
if strings.ContainsAny(s, forbiddenContainerRunes) {
14+
return ErrInvalidContainerName{}
15+
}
16+
17+
// The input could (and should) already have been escaped. This cycle
18+
// checks for the escaped versions of the forbidden characters. Note
19+
// that a simple "contains" is sufficient, because Go's http library
20+
// won't accept invalid escape sequences (e.g. "%%2F").
21+
for _, r := range forbiddenContainerRunes {
22+
if strings.Contains(strings.ToLower(s), fmt.Sprintf("%%%x", r)) {
23+
return ErrInvalidContainerName{}
24+
}
25+
}
26+
return nil
27+
}
428

529
func listURL(c *gophercloud.ServiceClient) string {
630
return c.Endpoint
731
}
832

9-
func createURL(c *gophercloud.ServiceClient, container string) string {
10-
return c.ServiceURL(container)
33+
func createURL(c *gophercloud.ServiceClient, container string) (string, error) {
34+
if err := CheckContainerName(container); err != nil {
35+
return "", err
36+
}
37+
return c.ServiceURL(container), nil
1138
}
1239

13-
func getURL(c *gophercloud.ServiceClient, container string) string {
40+
func getURL(c *gophercloud.ServiceClient, container string) (string, error) {
1441
return createURL(c, container)
1542
}
1643

17-
func deleteURL(c *gophercloud.ServiceClient, container string) string {
44+
func deleteURL(c *gophercloud.ServiceClient, container string) (string, error) {
1845
return createURL(c, container)
1946
}
2047

21-
func updateURL(c *gophercloud.ServiceClient, container string) string {
48+
func updateURL(c *gophercloud.ServiceClient, container string) (string, error) {
2249
return createURL(c, container)
2350
}
2451

0 commit comments

Comments
 (0)