Skip to content

Commit 8574146

Browse files
author
Quentin Brosse
authored
feat: client validation (#238)
1 parent 937624f commit 8574146

File tree

10 files changed

+181
-76
lines changed

10 files changed

+181
-76
lines changed

api/instance/v1/server_utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ import (
1010
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v1"
1111
"github.com/scaleway/scaleway-sdk-go/internal/async"
1212
"github.com/scaleway/scaleway-sdk-go/internal/errors"
13-
"github.com/scaleway/scaleway-sdk-go/internal/uuid"
13+
"github.com/scaleway/scaleway-sdk-go/internal/validation"
1414
"github.com/scaleway/scaleway-sdk-go/scw"
1515
)
1616

1717
// CreateServer creates a server.
1818
func (s *API) CreateServer(req *CreateServerRequest, opts ...scw.RequestOption) (*CreateServerResponse, error) {
1919

2020
// If image is not a UUID we try to fetch it from marketplace.
21-
if req.Image != "" && !uuid.IsUUID(req.Image) {
21+
if req.Image != "" && !validation.IsUUID(req.Image) {
2222
apiMarketplace := marketplace.NewAPI(s.client)
2323
imageId, err := apiMarketplace.GetLocalImageIDByLabel(&marketplace.GetLocalImageIDByLabelRequest{
2424
ImageLabel: req.Image,

internal/uuid/uuid.go

Lines changed: 0 additions & 10 deletions
This file was deleted.

internal/validation/is.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package validation
2+
3+
import (
4+
"net/url"
5+
"regexp"
6+
)
7+
8+
var (
9+
isUUIDRegexp = regexp.MustCompile(`[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}`)
10+
isRegionRegex = regexp.MustCompile("^[a-z]{2}-[a-z]{3}$")
11+
isZoneRegex = regexp.MustCompile("^[a-z]{2}-[a-z]{3}-[1-9]$")
12+
isAccessKey = regexp.MustCompile("^SCW[A-Z0-9]{17}$")
13+
)
14+
15+
// IsUUID returns true if the given string has a valid UUID format.
16+
func IsUUID(s string) bool {
17+
return isUUIDRegexp.MatchString(s)
18+
}
19+
20+
// IsAccessKey returns true if the given string has a valid Scaleway access key format.
21+
func IsAccessKey(s string) bool {
22+
return isAccessKey.MatchString(s)
23+
}
24+
25+
// IsSecretKey returns true if the given string has a valid Scaleway secret key format.
26+
func IsSecretKey(s string) bool {
27+
return IsUUID(s)
28+
}
29+
30+
// IsOrganizationID returns true if the given string has a valid Scaleway organization ID format.
31+
func IsOrganizationID(s string) bool {
32+
return IsUUID(s)
33+
}
34+
35+
// IsRegion returns true if the given string has a valid region format.
36+
func IsRegion(s string) bool {
37+
return isRegionRegex.MatchString(s)
38+
}
39+
40+
// IsZone returns true if the given string has a valid zone format.
41+
func IsZone(s string) bool {
42+
return isZoneRegex.MatchString(s)
43+
}
44+
45+
// IsURL returns true if the given string has a valid URL format.
46+
func IsURL(s string) bool {
47+
_, err := url.Parse(s)
48+
return err == nil
49+
}

scw/client_option.go

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package scw
22

33
import (
44
"net/http"
5-
"net/url"
5+
"strings"
66

77
"github.com/scaleway/scaleway-sdk-go/internal/auth"
88
"github.com/scaleway/scaleway-sdk-go/internal/errors"
9+
"github.com/scaleway/scaleway-sdk-go/internal/validation"
910
)
1011

1112
// ClientOption is a function which applies options to a settings object.
@@ -170,40 +171,70 @@ func (s *settings) apply(opts []ClientOption) {
170171
}
171172

172173
func (s *settings) validate() error {
173-
var err error
174+
// Auth.
174175
if s.token == nil {
175176
// It should not happen, WithoutAuth option is used by default.
176177
panic(errors.New("no credential option provided"))
177178
}
178-
179179
if token, isToken := s.token.(*auth.Token); isToken {
180180
if token.AccessKey == "" {
181-
return &ClientCredentialError{errorType: clientCredentialError_EmptyAccessKey}
181+
return NewInvalidClientOptionError("access key cannot be empty")
182+
}
183+
if !validation.IsAccessKey(token.AccessKey) {
184+
return NewInvalidClientOptionError("invalid access key format '%s', expected SCWXXXXXXXXXXXXXXXXX format", token.AccessKey)
182185
}
183186
if token.SecretKey == "" {
184-
return &ClientCredentialError{errorType: clientCredentialError_EmptySecreyKey}
187+
return NewInvalidClientOptionError("secret key cannot be empty")
188+
}
189+
if !validation.IsSecretKey(token.SecretKey) {
190+
return NewInvalidClientOptionError("invalid secret key format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", token.SecretKey)
185191
}
186192
}
187193

188-
_, err = url.Parse(s.apiURL)
189-
if err != nil {
190-
return errors.Wrap(err, "invalid url %s", s.apiURL)
194+
// Default Organization ID.
195+
if s.defaultOrganizationID != nil {
196+
if *s.defaultOrganizationID == "" {
197+
return NewInvalidClientOptionError("default organization ID cannot be empty")
198+
}
199+
if !validation.IsOrganizationID(*s.defaultOrganizationID) {
200+
return NewInvalidClientOptionError("invalid organization ID format '%s', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", *s.defaultOrganizationID)
201+
}
191202
}
192203

193-
// TODO: Check OrganizationID format
194-
if s.defaultOrganizationID != nil && *s.defaultOrganizationID == "" {
195-
return errors.New("default organization id cannot be empty")
204+
// Default Region.
205+
if s.defaultRegion != nil {
206+
if *s.defaultRegion == "" {
207+
return NewInvalidClientOptionError("default region cannot be empty")
208+
}
209+
if !validation.IsRegion(string(*s.defaultRegion)) {
210+
regions := []string(nil)
211+
for _, r := range AllRegions {
212+
regions = append(regions, string(r))
213+
}
214+
return NewInvalidClientOptionError("invalid default region format '%s', available regions are: %s", *s.defaultRegion, strings.Join(regions, ", "))
215+
}
196216
}
197217

198-
// TODO: Check Region format
199-
if s.defaultRegion != nil && *s.defaultRegion == "" {
200-
return errors.New("default region cannot be empty")
218+
// Default Zone.
219+
if s.defaultZone != nil {
220+
if *s.defaultZone == "" {
221+
return NewInvalidClientOptionError("default zone cannot be empty")
222+
}
223+
if !validation.IsZone(string(*s.defaultZone)) {
224+
zones := []string(nil)
225+
for _, z := range AllZones {
226+
zones = append(zones, string(z))
227+
}
228+
return NewInvalidClientOptionError("invalid default zone format '%s', available zones are: %s", *s.defaultZone, strings.Join(zones, ", "))
229+
}
201230
}
202231

203-
// TODO: Check Zone format
204-
if s.defaultZone != nil && *s.defaultZone == "" {
205-
return errors.New("default zone cannot be empty")
232+
// API URL.
233+
if !validation.IsURL(s.apiURL) {
234+
return NewInvalidClientOptionError("invalid url %s", s.apiURL)
206235
}
207236

237+
// TODO: check for max s.defaultPageSize
238+
208239
return nil
209240
}

scw/client_option_test.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,45 +29,64 @@ func TestClientOptions(t *testing.T) {
2929
{
3030
name: "Create a valid client option",
3131
clientOption: func(s *settings) {
32-
s.token = auth.NewToken(testAccessKey, testSecretKey)
33-
s.apiURL = apiURL
32+
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
33+
s.apiURL = v2ValidAPIURL
3434
s.defaultOrganizationID = &defaultOrganizationID
3535
s.defaultRegion = &defaultRegion
3636
s.defaultZone = &defaultZone
3737
},
3838
},
3939
{
40-
name: "Should throw an access key error",
40+
name: "Should throw an empty access key error",
4141
clientOption: func(s *settings) {
42-
s.apiURL = apiURL
43-
s.token = auth.NewToken("", testSecretKey)
42+
s.token = auth.NewToken("", v2ValidSecretKey)
4443
},
4544
errStr: "scaleway-sdk-go: access key cannot be empty",
4645
},
4746
{
48-
name: "Should throw a secret key error",
47+
name: "Should throw a bad access key error",
4948
clientOption: func(s *settings) {
50-
s.apiURL = apiURL
51-
s.token = auth.NewToken(testSecretKey, "")
49+
s.token = auth.NewToken(v2InvalidAccessKey, v2ValidSecretKey)
50+
},
51+
errStr: "scaleway-sdk-go: invalid access key format 'invalid', expected SCWXXXXXXXXXXXXXXXXX format",
52+
},
53+
{
54+
name: "Should throw an empty secret key error",
55+
clientOption: func(s *settings) {
56+
s.token = auth.NewToken(v2ValidAccessKey, "")
5257
},
5358
errStr: "scaleway-sdk-go: secret key cannot be empty",
5459
},
60+
{
61+
name: "Should throw a bad secret key error",
62+
clientOption: func(s *settings) {
63+
s.token = auth.NewToken(v2ValidAccessKey, v2InvalidSecretKey)
64+
},
65+
errStr: "scaleway-sdk-go: invalid secret key format 'invalid', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
66+
},
5567
{
5668
name: "Should throw an url error",
5769
clientOption: func(s *settings) {
5870
s.apiURL = ":test"
59-
s.token = auth.NewToken(testAccessKey, testSecretKey)
71+
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
6072
},
61-
errStr: "scaleway-sdk-go: invalid url :test: parse :test: missing protocol scheme",
73+
errStr: "scaleway-sdk-go: invalid url :test",
6274
},
6375
{
64-
name: "Should throw a organization id error",
76+
name: "Should throw an empty organization ID error",
6577
clientOption: func(s *settings) {
66-
v := ""
67-
s.token = auth.NewToken(testAccessKey, testSecretKey)
68-
s.defaultOrganizationID = &v
78+
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
79+
s.defaultOrganizationID = StringPtr("")
6980
},
70-
errStr: "scaleway-sdk-go: default organization id cannot be empty",
81+
errStr: "scaleway-sdk-go: default organization ID cannot be empty",
82+
},
83+
{
84+
name: "Should throw a bad organization ID error",
85+
clientOption: func(s *settings) {
86+
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
87+
s.defaultOrganizationID = StringPtr(v2InvalidDefaultOrganizationID)
88+
},
89+
errStr: "scaleway-sdk-go: invalid organization ID format 'invalid', expected a UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
7190
},
7291
{
7392
name: "Should throw a region error",
@@ -78,6 +97,15 @@ func TestClientOptions(t *testing.T) {
7897
},
7998
errStr: "scaleway-sdk-go: default region cannot be empty",
8099
},
100+
{
101+
name: "Should throw a bad region error",
102+
clientOption: func(s *settings) {
103+
v := Region(v2InvalidDefaultRegion)
104+
s.token = auth.NewToken(testAccessKey, testSecretKey)
105+
s.defaultRegion = &v
106+
},
107+
errStr: "scaleway-sdk-go: invalid default region format 'invalid', available regions are: fr-par, nl-ams",
108+
},
81109
{
82110
name: "Should throw a zone error",
83111
clientOption: func(s *settings) {
@@ -87,6 +115,15 @@ func TestClientOptions(t *testing.T) {
87115
},
88116
errStr: "scaleway-sdk-go: default zone cannot be empty",
89117
},
118+
{
119+
name: "Should throw a bad zone error",
120+
clientOption: func(s *settings) {
121+
v := Zone(v2InvalidDefaultZone)
122+
s.token = auth.NewToken(testAccessKey, testSecretKey)
123+
s.defaultZone = &v
124+
},
125+
errStr: "scaleway-sdk-go: invalid default zone format 'invalid', available zones are: fr-par-1, fr-par-2, nl-ams-1",
126+
},
90127
}
91128

92129
for _, c := range testCases {

scw/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
const (
1616
testAPIURL = "https://api.example.com/"
1717
defaultAPIURL = "https://api.scaleway.com"
18-
testAccessKey = "ACCESS_KEY"
18+
testAccessKey = "SCW1234567890ABCDEFG"
1919
testSecretKey = "7363616c-6577-6573-6862-6f7579616161" // hint: | xxd -ps -r
2020
testDefaultOrganizationID = "6170692e-7363-616c-6577-61792e636f6d" // hint: | xxd -ps -r
2121
testDefaultRegion = RegionFrPar

scw/config_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,15 +404,15 @@ const emptyFile = ""
404404

405405
// v2 config
406406
var (
407-
v2ValidAccessKey2 = "ACCESS_KEY2"
407+
v2ValidAccessKey2 = "SCW234567890ABCDEFGH"
408408
v2ValidSecretKey2 = "6f6e6574-6f72-756c-6c74-68656d616c6c" // hint: | xxd -ps -r
409409
v2ValidAPIURL2 = "api-fr-par.scaleway.com"
410410
v2ValidInsecure2 = "true"
411411
v2ValidDefaultOrganizationID2 = "6d6f7264-6f72-6772-6561-74616761696e" // hint: | xxd -ps -r
412412
v2ValidDefaultRegion2 = string(RegionFrPar)
413413
v2ValidDefaultZone2 = string(ZoneFrPar2)
414414

415-
v2ValidAccessKey = "ACCESS_KEY"
415+
v2ValidAccessKey = "SCW1234567890ABCDEFG"
416416
v2ValidSecretKey = "7363616c-6577-6573-6862-6f7579616161" // hint: | xxd -ps -r
417417
v2ValidAPIURL = "api.scaleway.com"
418418
v2ValidInsecure = "false"
@@ -421,6 +421,12 @@ var (
421421
v2ValidDefaultZone = string(ZoneNlAms1)
422422
v2ValidProfile = "flantier"
423423

424+
v2InvalidAccessKey = "invalid"
425+
v2InvalidSecretKey = "invalid"
426+
v2InvalidDefaultOrganizationID = "invalid"
427+
v2InvalidDefaultRegion = "invalid"
428+
v2InvalidDefaultZone = "invalid"
429+
424430
v2SimpleValidConfig = &Config{
425431
Profile: Profile{
426432
AccessKey: &v2ValidAccessKey,
@@ -500,19 +506,19 @@ func TestConfigString(t *testing.T) {
500506
},
501507
}
502508

503-
testhelpers.Equals(t, `access_key: ACCESS_KEY
509+
testhelpers.Equals(t, `access_key: SCW1234567890ABCDEFG
504510
secret_key: 7363616c-xxxx-xxxx-xxxx-xxxxxxxxxxxx
505511
active_profile: flantier
506512
profiles:
507513
flantier:
508-
access_key: ACCESS_KEY2
514+
access_key: SCW234567890ABCDEFGH
509515
secret_key: 6f6e6574-xxxx-xxxx-xxxx-xxxxxxxxxxxx
510516
`, c.String())
511517
testhelpers.Equals(t, v2ValidSecretKey, *c.SecretKey)
512518

513519
p, err := c.GetActiveProfile()
514520
testhelpers.AssertNoError(t, err)
515-
testhelpers.Equals(t, `access_key: ACCESS_KEY2
521+
testhelpers.Equals(t, `access_key: SCW234567890ABCDEFGH
516522
secret_key: 6f6e6574-xxxx-xxxx-xxxx-xxxxxxxxxxxx
517523
`, p.String())
518524
testhelpers.Equals(t, v2ValidSecretKey2, *p.SecretKey)

scw/errors.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,20 +256,17 @@ func (e *OutOfStockError) GetRawBody() json.RawMessage {
256256
return e.RawBody
257257
}
258258

259-
type clientCredentialErrorType string
260-
261-
const (
262-
clientCredentialError_EmptyAccessKey = clientCredentialErrorType("access key cannot be empty")
263-
clientCredentialError_EmptySecreyKey = clientCredentialErrorType("secret key cannot be empty")
264-
)
259+
// InvalidClientOptionError indicates that at least one of client data has been badly provided for the client creation.
260+
type InvalidClientOptionError struct {
261+
errorType string
262+
}
265263

266-
// clientCredentialError indicates that credentials have been badly provided for the client creation.
267-
type ClientCredentialError struct {
268-
errorType clientCredentialErrorType
264+
func NewInvalidClientOptionError(format string, a ...interface{}) *InvalidClientOptionError {
265+
return &InvalidClientOptionError{errorType: fmt.Sprintf(format, a...)}
269266
}
270267

271268
// IsScwSdkError implements the SdkError interface
272-
func (e ClientCredentialError) IsScwSdkError() {}
273-
func (e ClientCredentialError) Error() string {
269+
func (e InvalidClientOptionError) IsScwSdkError() {}
270+
func (e InvalidClientOptionError) Error() string {
274271
return fmt.Sprintf("scaleway-sdk-go: %s", e.errorType)
275272
}

0 commit comments

Comments
 (0)