Skip to content

Commit

Permalink
fix(serviceaccount): add input validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Enda Phelan committed Mar 30, 2021
1 parent 6f2f44c commit ec76591
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 13 deletions.
2 changes: 1 addition & 1 deletion cmd/rhoas/pkged.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/openconfig/goyang v0.2.4
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
Expand Down
2 changes: 1 addition & 1 deletion locales/cmd/kafka/topic/common/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ one = 'topic name is required'
one = 'topic name cannot exceed {{.MaxNameLen}} characters'

[kafka.topic.common.validation.name.error.invalidChars]
one = 'invalid topic name "{{.Name}}", only letters (Aa-Zz), numbers, "_" and "-" are accepted'
one = 'invalid topic name "{{.Name}}"; only letters (Aa-Zz), numbers, "_" and "-" are accepted'

[kafka.topic.common.validation.partitions.error.invalid]
one = 'invalid partition count {{.Partitions}}, minimum partition count is {{.MinPartitions}}'
Expand Down
15 changes: 15 additions & 0 deletions locales/cmd/serviceaccount/active.en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,18 @@ one = 'Sets a custom file location to save the credentials.'
[serviceAccount.common.log.debug.interactive.fileFormatNotSet]
description = 'debug message'
one = '--file-format flag is not set, prompting user to enter a value'

[serviceAccount.common.validation.name.error.required]
one = 'service account name is required'

[serviceAccount.common.validation.name.error.lengthError]
one = 'service account name cannot exceed {{.MaxNameLen}} characters'

[serviceAccount.common.validation.name.error.invalidChars]
one = 'invalid service account name "{{.Name}}"; only lowercase letters (a-z), numbers, and "-" are accepted'

[serviceAccount.common.validation.description.error.invalidChars]
one = 'invalid service account description "{{.Description}}"; only lowercase letters (a-z) and numbers are accepted'

[serviceAccount.common.validation.description.error.lengthError]
one = 'service account description cannot exceed {{.MaxNameLen}} characters'
32 changes: 22 additions & 10 deletions pkg/cmd/serviceaccount/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"os"

"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/serviceaccount/validation"

kasclient "github.com/bf2fc6cc711aee1a0c2a/cli/pkg/api/kas/client"
"github.com/bf2fc6cc711aee1a0c2a/cli/pkg/connection"

Expand Down Expand Up @@ -50,7 +52,8 @@ func NewCreateCommand(f *factory.Factory) *cobra.Command {
Short: localizer.MustLocalizeFromID("serviceAccount.create.cmd.shortDescription"),
Long: localizer.MustLocalizeFromID("serviceAccount.create.cmd.longDescription"),
Example: localizer.MustLocalizeFromID("serviceAccount.create.cmd.example"),
RunE: func(cmd *cobra.Command, _ []string) error {
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) (err error) {
if !opts.IO.CanPrompt() && opts.name == "" {
return fmt.Errorf(localizer.MustLocalize(&localizer.Config{
MessageID: "flag.error.requiredWhenNonInteractive",
Expand All @@ -62,13 +65,22 @@ func NewCreateCommand(f *factory.Factory) *cobra.Command {
opts.interactive = true
}

if !opts.interactive && opts.fileFormat == "" {
return fmt.Errorf(localizer.MustLocalize(&localizer.Config{
MessageID: "flag.error.requiredFlag",
TemplateData: map[string]interface{}{
"Flag": "file-format",
},
}))
if !opts.interactive {
if opts.fileFormat == "" {
return fmt.Errorf(localizer.MustLocalize(&localizer.Config{
MessageID: "flag.error.requiredFlag",
TemplateData: map[string]interface{}{
"Flag": "file-format",
},
}))
}

if err = validation.ValidateName(opts.name); err != nil {
return err
}
if err = validation.ValidateDescription(opts.description); err != nil {
return err
}
}

// check that a valid --file-format flag value is used
Expand Down Expand Up @@ -199,7 +211,7 @@ func runInteractivePrompt(opts *Options) (err error) {
Help: localizer.MustLocalizeFromID("serviceAccount.create.input.name.help"),
}

err = survey.AskOne(promptName, &opts.name, survey.WithValidator(survey.Required))
err = survey.AskOne(promptName, &opts.name, survey.WithValidator(survey.Required), survey.WithValidator(validation.ValidateName))
if err != nil {
return err
}
Expand Down Expand Up @@ -228,7 +240,7 @@ func runInteractivePrompt(opts *Options) (err error) {

promptDescription := &survey.Multiline{Message: localizer.MustLocalizeFromID("serviceAccount.create.input.description.message")}

err = survey.AskOne(promptDescription, &opts.description)
err = survey.AskOne(promptDescription, &opts.description, survey.WithValidator(validation.ValidateDescription))
if err != nil {
return err
}
Expand Down
96 changes: 96 additions & 0 deletions pkg/serviceaccount/validation/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package validation

import (
"errors"
"regexp"

"github.com/bf2fc6cc711aee1a0c2a/cli/internal/localizer"
)

const (
// name validation rules
legalNameChars = "^[a-z]([-a-z0-9]*[a-z0-9])?$"
maxNameLength = 50
minNameLength = 1
// description validation rules
legalDescriptionChars = "^[a-z0-9\\s]*$"
maxDescriptionLength = 255
)

// ValidateName validates the name of the service account
func ValidateName(val interface{}) error {
name, ok := val.(string)
if !ok {
return errors.New(localizer.MustLocalize(&localizer.Config{
MessageID: "common.error.castError",
TemplateData: map[string]interface{}{
"Value": val,
"Type": "string",
},
}))
}

if len(name) < minNameLength {
return errors.New(localizer.MustLocalizeFromID("serviceAccount.common.validation.name.error.required"))
} else if len(name) > maxNameLength {
return errors.New(localizer.MustLocalize(&localizer.Config{
MessageID: "serviceAccount.common.validation.name.error.lengthError",
TemplateData: map[string]interface{}{
"MaxNameLen": maxNameLength,
},
}))
}

matched, _ := regexp.Match(legalNameChars, []byte(name))

if matched {
return nil
}

return errors.New(localizer.MustLocalize(&localizer.Config{
MessageID: "serviceAccount.common.validation.name.error.invalidChars",
TemplateData: map[string]interface{}{
"Name": name,
},
}))
}

// ValidateDescription validates the service account description text
func ValidateDescription(val interface{}) error {
description, ok := val.(string)
if !ok {
return errors.New(localizer.MustLocalize(&localizer.Config{
MessageID: "common.error.castError",
TemplateData: map[string]interface{}{
"Value": val,
"Type": "string",
},
}))
}

if description == "" {
return nil
}

if len(description) > maxDescriptionLength {
return errors.New(localizer.MustLocalize(&localizer.Config{
MessageID: "serviceAccount.common.validation.description.error.lengthError",
TemplateData: map[string]interface{}{
"MaxNameLen": maxDescriptionLength,
},
}))
}

matched, _ := regexp.Match(legalDescriptionChars, []byte(description))

if matched {
return nil
}

return errors.New(localizer.MustLocalize(&localizer.Config{
MessageID: "serviceAccount.common.validation.description.error.invalidChars",
TemplateData: map[string]interface{}{
"Description": description,
},
}))
}
126 changes: 126 additions & 0 deletions pkg/serviceaccount/validation/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package validation

import (
"testing"

"github.com/bf2fc6cc711aee1a0c2a/cli/internal/localizer"
)

func TestValidateName(t *testing.T) {
_ = localizer.IncludeAssetsAndLoadMessageFiles()

type args struct {
val interface{}
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "fails when length is 0",
args: args{""},
wantErr: true,
},
{
name: "passes when length is 1",
args: args{"s"},
wantErr: false,
},
{
name: "passes when length is 50 (max length)",
args: args{"ssssssssssssssssssssssssssssssssssssssssssssssssss"},
wantErr: false,
},
{
name: "fails when length exceeds max length",
args: args{"sssssssssssssssssssssssssssssssssssssssssssssssssss"},
wantErr: true,
},
{
name: "passes on valid name",
args: args{"svcacctone"},
wantErr: false,
},
{
name: "passes on valid name with hyphens",
args: args{"svc-acct-one"},
wantErr: false,
},
{
name: "passes on valid name with digits",
args: args{"svc-acct-1s"},
wantErr: false,
},
{
name: "fails with capital letters",
args: args{"Svc-acct-one"},
wantErr: true,
},
{
name: "fails number in first section",
args: args{"1svc-acct-one"},
wantErr: true,
},
}
// nolint:scopelint
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidateName(tt.args.val); (err != nil) != tt.wantErr {
t.Errorf("ValidateName() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestValidateDescription(t *testing.T) {
_ = localizer.IncludeAssetsAndLoadMessageFiles()

type args struct {
val interface{}
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "passes when empty",
args: args{""},
wantErr: false,
},
{
name: "passes on max length (255)",
args: args{"trl1rmcyl6dp4xxqy0rwudhodbpjc4crja8ibf2yco6obalko6qor9n2a1wsqruolg0ewrndumw2xkezzuwg8pjo6ntsmi1cjw99hjcko4t2kjkxmaswzgk8ko75pcs4js0pzypuyjxxnld4dijxadzs8peioi6d5jjxxtfl9vicufmxuacvu7m8ycbwhsbiu9ipw5fxplf0ojs8bxd7hwt4rn4phbcdgivxdzprhyfjamkgjzytjz25cmqagtw"},
wantErr: false,
},
{
name: "fails when exceeds max length",
args: args{"trl1rmcyl6dp4xxqy0rwudhodbpjc4crja8ibf2yco6obalko6qor9n2a1wsqruolg0ewrndumw2xkezzuwg8pjo6ntsmi1cjw99hjcko4t2kjkxmaswzgk8ko75pcs4js0pzypuyjxxnld4dijxadzs8peioi6d5jjxxtfl9vicufmxuacvu7m8ycbwhsbiu9ipw5fxplf0ojs8bxd7hwt4rn4phbcdgivxdzprhyfjamkgjzytjz25cmqagtwa"},
wantErr: true,
},
{
name: "passes with spaces",
args: args{"here is a description"},
wantErr: false,
},
{
name: "fails with special character",
args: args{"here is a description!"},
wantErr: true,
},
{
name: "fails with capital letters",
args: args{"Hello"},
wantErr: true,
},
}
for _, tt := range tests {
// nolint:scopelint
t.Run(tt.name, func(t *testing.T) {
if err := ValidateDescription(tt.args.val); (err != nil) != tt.wantErr {
t.Errorf("ValidateDescription() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

0 comments on commit ec76591

Please sign in to comment.