Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client validation #238

Merged
merged 4 commits into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/instance/v1/server_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import (
"github.com/scaleway/scaleway-sdk-go/api/marketplace/v1"
"github.com/scaleway/scaleway-sdk-go/internal/async"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/internal/uuid"
"github.com/scaleway/scaleway-sdk-go/internal/validation"
"github.com/scaleway/scaleway-sdk-go/scw"
)

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

// If image is not a UUID we try to fetch it from marketplace.
if req.Image != "" && !uuid.IsUUID(req.Image) {
if req.Image != "" && !validation.IsUUID(req.Image) {
apiMarketplace := marketplace.NewAPI(s.client)
imageId, err := apiMarketplace.GetLocalImageIDByLabel(&marketplace.GetLocalImageIDByLabelRequest{
ImageLabel: req.Image,
Expand Down
10 changes: 0 additions & 10 deletions internal/uuid/uuid.go

This file was deleted.

49 changes: 49 additions & 0 deletions internal/validation/is.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package validation

import (
"net/url"
"regexp"
)

var (
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}`)
isRegionRegex = regexp.MustCompile("^[a-z]{2}-[a-z]{3}$")
isZoneRegex = regexp.MustCompile("^[a-z]{2}-[a-z]{3}-[1-9]$")
QuentinBrosse marked this conversation as resolved.
Show resolved Hide resolved
isAccessKey = regexp.MustCompile("^SCW[A-Z0-9]{17}$")
)

// IsUUID returns true if the given string has a valid UUID format.
func IsUUID(s string) bool {
return isUUIDRegexp.MatchString(s)
}

// IsAccessKey returns true if the given string has a valid Scaleway access key format.
func IsAccessKey(s string) bool {
return isAccessKey.MatchString(s)
}

// IsSecretKey returns true if the given string has a valid Scaleway secret key format.
func IsSecretKey(s string) bool {
return IsUUID(s)
}

// IsOrganizationID returns true if the given string has a valid Scaleway organization ID format.
func IsOrganizationID(s string) bool {
return IsUUID(s)
}

// IsRegion returns true if the given string has a valid region format.
func IsRegion(s string) bool {
return isRegionRegex.MatchString(s)
}

// IsZone returns true if the given string has a valid zone format.
func IsZone(s string) bool {
return isZoneRegex.MatchString(s)
}

// IsURL returns true if the given string has a valid URL format.
func IsURL(s string) bool {
_, err := url.Parse(s)
return err == nil
}
65 changes: 48 additions & 17 deletions scw/client_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package scw

import (
"net/http"
"net/url"
"strings"

"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/internal/validation"
)

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

func (s *settings) validate() error {
var err error
// Auth.
if s.token == nil {
// It should not happen, WithoutAuth option is used by default.
panic(errors.New("no credential option provided"))
}

if token, isToken := s.token.(*auth.Token); isToken {
if token.AccessKey == "" {
return &ClientCredentialError{errorType: clientCredentialError_EmptyAccessKey}
return NewClientValidationError("access key cannot be empty")
}
if !validation.IsAccessKey(token.AccessKey) {
return NewClientValidationError("bad access key format")
QuentinBrosse marked this conversation as resolved.
Show resolved Hide resolved
}
if token.SecretKey == "" {
return &ClientCredentialError{errorType: clientCredentialError_EmptySecreyKey}
return NewClientValidationError("secret key cannot be empty")
}
if !validation.IsSecretKey(token.SecretKey) {
return NewClientValidationError("bad secret key format")
QuentinBrosse marked this conversation as resolved.
Show resolved Hide resolved
}
}

_, err = url.Parse(s.apiURL)
if err != nil {
return errors.Wrap(err, "invalid url %s", s.apiURL)
// Default Organization ID.
if s.defaultOrganizationID != nil {
if *s.defaultOrganizationID == "" {
return NewClientValidationError("default organization ID cannot be empty")
}
if !validation.IsOrganizationID(*s.defaultOrganizationID) {
return NewClientValidationError("default organization ID must be a valid UUID")
QuentinBrosse marked this conversation as resolved.
Show resolved Hide resolved
}
}

// TODO: Check OrganizationID format
if s.defaultOrganizationID != nil && *s.defaultOrganizationID == "" {
return errors.New("default organization id cannot be empty")
// Default Region.
if s.defaultRegion != nil {
if *s.defaultRegion == "" {
return NewClientValidationError("default region cannot be empty")
}
if !validation.IsRegion(string(*s.defaultRegion)) {
regions := []string(nil)
for _, r := range AllRegions {
regions = append(regions, string(r))
}
return NewClientValidationError("bad default region format, available regions are: %s", strings.Join(regions, ", "))
QuentinBrosse marked this conversation as resolved.
Show resolved Hide resolved
}
}

// TODO: Check Region format
if s.defaultRegion != nil && *s.defaultRegion == "" {
return errors.New("default region cannot be empty")
// Default Zone.
if s.defaultZone != nil {
if *s.defaultZone == "" {
return NewClientValidationError("default zone cannot be empty")
}
if !validation.IsZone(string(*s.defaultZone)) {
zones := []string(nil)
for _, z := range AllZones {
zones = append(zones, string(z))
}
return NewClientValidationError("bad default zone format, available zones are: %s", strings.Join(zones, ", "))
}
}

// TODO: Check Zone format
if s.defaultZone != nil && *s.defaultZone == "" {
return errors.New("default zone cannot be empty")
// API URL.
if !validation.IsURL(s.apiURL) {
return NewClientValidationError("invalid url %s", s.apiURL)
}

// TODO: check for max s.defaultPageSize

return nil
}
67 changes: 52 additions & 15 deletions scw/client_option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,64 @@ func TestClientOptions(t *testing.T) {
{
name: "Create a valid client option",
clientOption: func(s *settings) {
s.token = auth.NewToken(testAccessKey, testSecretKey)
s.apiURL = apiURL
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
s.apiURL = v2ValidAPIURL
s.defaultOrganizationID = &defaultOrganizationID
s.defaultRegion = &defaultRegion
s.defaultZone = &defaultZone
},
},
{
name: "Should throw an access key error",
name: "Should throw an empty access key error",
clientOption: func(s *settings) {
s.apiURL = apiURL
s.token = auth.NewToken("", testSecretKey)
s.token = auth.NewToken("", v2ValidSecretKey)
},
errStr: "scaleway-sdk-go: access key cannot be empty",
},
{
name: "Should throw a secret key error",
name: "Should throw a bad access key error",
clientOption: func(s *settings) {
s.apiURL = apiURL
s.token = auth.NewToken(testSecretKey, "")
s.token = auth.NewToken(v2InvalidAccessKey, v2ValidSecretKey)
},
errStr: "scaleway-sdk-go: bad access key format",
},
{
name: "Should throw an empty secret key error",
clientOption: func(s *settings) {
s.token = auth.NewToken(v2ValidAccessKey, "")
},
errStr: "scaleway-sdk-go: secret key cannot be empty",
},
{
name: "Should throw a bad secret key error",
clientOption: func(s *settings) {
s.token = auth.NewToken(v2ValidAccessKey, v2InvalidSecretKey)
},
errStr: "scaleway-sdk-go: bad secret key format",
},
{
name: "Should throw an url error",
clientOption: func(s *settings) {
s.apiURL = ":test"
s.token = auth.NewToken(testAccessKey, testSecretKey)
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
},
errStr: "scaleway-sdk-go: invalid url :test: parse :test: missing protocol scheme",
errStr: "scaleway-sdk-go: invalid url :test",
},
{
name: "Should throw a organization id error",
name: "Should throw an empty organization ID error",
clientOption: func(s *settings) {
v := ""
s.token = auth.NewToken(testAccessKey, testSecretKey)
s.defaultOrganizationID = &v
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
s.defaultOrganizationID = StringPtr("")
},
errStr: "scaleway-sdk-go: default organization id cannot be empty",
errStr: "scaleway-sdk-go: default organization ID cannot be empty",
},
{
name: "Should throw a bad organization ID error",
clientOption: func(s *settings) {
s.token = auth.NewToken(v2ValidAccessKey, v2ValidSecretKey)
s.defaultOrganizationID = StringPtr(v2InvalidDefaultOrganizationID)
},
errStr: "scaleway-sdk-go: default organization ID must be a valid UUID",
},
{
name: "Should throw a region error",
Expand All @@ -78,6 +97,15 @@ func TestClientOptions(t *testing.T) {
},
errStr: "scaleway-sdk-go: default region cannot be empty",
},
{
name: "Should throw a bad region error",
clientOption: func(s *settings) {
v := Region(v2InvalidDefaultRegion)
s.token = auth.NewToken(testAccessKey, testSecretKey)
s.defaultRegion = &v
},
errStr: "scaleway-sdk-go: bad default region format, available regions are: fr-par, nl-ams",
},
{
name: "Should throw a zone error",
clientOption: func(s *settings) {
Expand All @@ -87,6 +115,15 @@ func TestClientOptions(t *testing.T) {
},
errStr: "scaleway-sdk-go: default zone cannot be empty",
},
{
name: "Should throw a bad zone error",
clientOption: func(s *settings) {
v := Zone(v2InvalidDefaultZone)
s.token = auth.NewToken(testAccessKey, testSecretKey)
s.defaultZone = &v
},
errStr: "scaleway-sdk-go: bad default zone format, available zones are: fr-par-1, fr-par-2, nl-ams-1",
},
}

for _, c := range testCases {
Expand Down
2 changes: 1 addition & 1 deletion scw/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
const (
testAPIURL = "https://api.example.com/"
defaultAPIURL = "https://api.scaleway.com"
testAccessKey = "ACCESS_KEY"
testAccessKey = "SCW1234567890ABCDEFG"
testSecretKey = "7363616c-6577-6573-6862-6f7579616161" // hint: | xxd -ps -r
testDefaultOrganizationID = "6170692e-7363-616c-6577-61792e636f6d" // hint: | xxd -ps -r
testDefaultRegion = RegionFrPar
Expand Down
16 changes: 11 additions & 5 deletions scw/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,15 +404,15 @@ const emptyFile = ""

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

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

v2InvalidAccessKey = "invalid"
v2InvalidSecretKey = "invalid"
v2InvalidDefaultOrganizationID = "invalid"
v2InvalidDefaultRegion = "invalid"
v2InvalidDefaultZone = "invalid"

v2SimpleValidConfig = &Config{
Profile: Profile{
AccessKey: &v2ValidAccessKey,
Expand Down Expand Up @@ -500,19 +506,19 @@ func TestConfigString(t *testing.T) {
},
}

testhelpers.Equals(t, `access_key: ACCESS_KEY
testhelpers.Equals(t, `access_key: SCW1234567890ABCDEFG
secret_key: 7363616c-xxxx-xxxx-xxxx-xxxxxxxxxxxx
active_profile: flantier
profiles:
flantier:
access_key: ACCESS_KEY2
access_key: SCW234567890ABCDEFGH
secret_key: 6f6e6574-xxxx-xxxx-xxxx-xxxxxxxxxxxx
`, c.String())
testhelpers.Equals(t, v2ValidSecretKey, *c.SecretKey)

p, err := c.GetActiveProfile()
testhelpers.AssertNoError(t, err)
testhelpers.Equals(t, `access_key: ACCESS_KEY2
testhelpers.Equals(t, `access_key: SCW234567890ABCDEFGH
secret_key: 6f6e6574-xxxx-xxxx-xxxx-xxxxxxxxxxxx
`, p.String())
testhelpers.Equals(t, v2ValidSecretKey2, *p.SecretKey)
Expand Down
19 changes: 8 additions & 11 deletions scw/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,20 +256,17 @@ func (e *OutOfStockError) GetRawBody() json.RawMessage {
return e.RawBody
}

type clientCredentialErrorType string

const (
clientCredentialError_EmptyAccessKey = clientCredentialErrorType("access key cannot be empty")
clientCredentialError_EmptySecreyKey = clientCredentialErrorType("secret key cannot be empty")
)
// ClientValidationError indicates that at least one of client data have been badly provided for the client creation.
type ClientValidationError struct {
QuentinBrosse marked this conversation as resolved.
Show resolved Hide resolved
errorType string
}

// clientCredentialError indicates that credentials have been badly provided for the client creation.
type ClientCredentialError struct {
errorType clientCredentialErrorType
func NewClientValidationError(format string, a ...interface{}) *ClientValidationError {
return &ClientValidationError{errorType: fmt.Sprintf(format, a...)}
}

// IsScwSdkError implements the SdkError interface
func (e ClientCredentialError) IsScwSdkError() {}
func (e ClientCredentialError) Error() string {
func (e ClientValidationError) IsScwSdkError() {}
func (e ClientValidationError) Error() string {
return fmt.Sprintf("scaleway-sdk-go: %s", e.errorType)
}
Loading