From 4bc6cfdd0acce665a80663db2766be2d7365353a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 4 Sep 2024 05:41:56 +0000 Subject: [PATCH] feat(codersdk): export name validators --- cli/create.go | 15 ++-- cli/organizationmanage.go | 7 +- cli/templatepush.go | 13 +++- cli/usercreate.go | 18 ++++- coderd/httpapi/name.go | 125 ---------------------------------- enterprise/cli/groupcreate.go | 16 ++++- 6 files changed, 59 insertions(+), 135 deletions(-) delete mode 100644 coderd/httpapi/name.go diff --git a/cli/create.go b/cli/create.go index bdf805ee26d69..5384ec094fd73 100644 --- a/cli/create.go +++ b/cli/create.go @@ -60,9 +60,13 @@ func (r *RootCmd) create() *serpent.Command { workspaceName, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: "Specify a name for your workspace:", Validate: func(workspaceName string) error { - _, err = client.WorkspaceByOwnerAndName(inv.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceOptions{}) + err = codersdk.NameValid(workspaceName) + if err != nil { + return xerrors.Errorf("workspace name %q is invalid: %w", workspaceName, err) + } + _, err = client.WorkspaceByOwnerAndName(inv.Context(), workspaceOwner, workspaceName, codersdk.WorkspaceOptions{}) if err == nil { - return xerrors.Errorf("A workspace already exists named %q!", workspaceName) + return xerrors.Errorf("a workspace already exists named %q", workspaceName) } return nil }, @@ -71,10 +75,13 @@ func (r *RootCmd) create() *serpent.Command { return err } } - + err = codersdk.NameValid(workspaceName) + if err != nil { + return xerrors.Errorf("workspace name %q is invalid: %w", workspaceName, err) + } _, err = client.WorkspaceByOwnerAndName(inv.Context(), workspaceOwner, workspaceName, codersdk.WorkspaceOptions{}) if err == nil { - return xerrors.Errorf("A workspace already exists named %q!", workspaceName) + return xerrors.Errorf("a workspace already exists named %q", workspaceName) } var sourceWorkspace codersdk.Workspace diff --git a/cli/organizationmanage.go b/cli/organizationmanage.go index f5cf001802536..82aa3e3b83e34 100644 --- a/cli/organizationmanage.go +++ b/cli/organizationmanage.go @@ -30,6 +30,11 @@ func (r *RootCmd) createOrganization() *serpent.Command { Handler: func(inv *serpent.Invocation) error { orgName := inv.Args[0] + err := codersdk.NameValid(orgName) + if err != nil { + return xerrors.Errorf("organization name %q is invalid: %w", orgName, err) + } + // This check is not perfect since not all users can read all organizations. // So ignore the error and if the org already exists, prevent the user // from creating it. @@ -38,7 +43,7 @@ func (r *RootCmd) createOrganization() *serpent.Command { return xerrors.Errorf("organization %q already exists", orgName) } - _, err := cliui.Prompt(inv, cliui.PromptOptions{ + _, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: fmt.Sprintf("Are you sure you want to create an organization with the name %s?\n%s", pretty.Sprint(cliui.DefaultStyles.Code, orgName), pretty.Sprint(cliui.BoldFmt(), "This action is irreversible."), diff --git a/cli/templatepush.go b/cli/templatepush.go index 078af4e3c6671..e8bba31c818cc 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -10,7 +10,6 @@ import ( "path/filepath" "strings" "time" - "unicode/utf8" "github.com/briandowns/spinner" "github.com/google/uuid" @@ -57,8 +56,16 @@ func (r *RootCmd) templatePush() *serpent.Command { return err } - if utf8.RuneCountInString(name) > 32 { - return xerrors.Errorf("Template name must be no more than 32 characters") + err = codersdk.NameValid(name) + if err != nil { + return xerrors.Errorf("template name %q is invalid: %w", name, err) + } + + if versionName != "" { + err = codersdk.TemplateVersionNameValid(versionName) + if err != nil { + return xerrors.Errorf("template version name %q is invalid: %w", versionName, err) + } } var createTemplate bool diff --git a/cli/usercreate.go b/cli/usercreate.go index d1ae2baf85a43..f73a3165ee908 100644 --- a/cli/usercreate.go +++ b/cli/usercreate.go @@ -44,6 +44,13 @@ func (r *RootCmd) userCreate() *serpent.Command { if username == "" { username, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: "Username:", + Validate: func(username string) error { + err = codersdk.NameValid(username) + if err != nil { + return xerrors.Errorf("username %q is invalid: %w", username, err) + } + return nil + }, }) if err != nil { return err @@ -144,7 +151,16 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`! Flag: "username", FlagShorthand: "u", Description: "Specifies a username for the new user.", - Value: serpent.StringOf(&username), + Value: serpent.Validate(serpent.StringOf(&username), func(_username *serpent.String) error { + username := _username.String() + if username != "" { + err := codersdk.NameValid(username) + if err != nil { + return xerrors.Errorf("username %q is invalid: %w", username, err) + } + } + return nil + }), }, { Flag: "full-name", diff --git a/coderd/httpapi/name.go b/coderd/httpapi/name.go deleted file mode 100644 index a40542fe2dd21..0000000000000 --- a/coderd/httpapi/name.go +++ /dev/null @@ -1,125 +0,0 @@ -package httpapi - -import ( - "regexp" - "strings" - - "github.com/moby/moby/pkg/namesgenerator" - "golang.org/x/xerrors" -) - -var ( - UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") - usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*") - - templateVersionName = regexp.MustCompile(`^[a-zA-Z0-9]+(?:[_.-]{1}[a-zA-Z0-9]+)*$`) - templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`) -) - -// UsernameFrom returns a best-effort username from the provided string. -// -// It first attempts to validate the incoming string, which will -// be returned if it is valid. It then will attempt to extract -// the username from an email address. If no success happens during -// these steps, a random username will be returned. -func UsernameFrom(str string) string { - if valid := NameValid(str); valid == nil { - return str - } - emailAt := strings.LastIndex(str, "@") - if emailAt >= 0 { - str = str[:emailAt] - } - str = usernameReplace.ReplaceAllString(str, "") - if valid := NameValid(str); valid == nil { - return str - } - return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-") -} - -// NameValid returns whether the input string is a valid name. -// It is a generic validator for any name that doesn't have it's own validator. -func NameValid(str string) error { - if len(str) > 32 { - return xerrors.New("must be <= 32 characters") - } - if len(str) < 1 { - return xerrors.New("must be >= 1 character") - } - // Avoid conflicts with routes like /templates/new and /groups/create. - if str == "new" || str == "create" { - return xerrors.Errorf("cannot use %q as a name", str) - } - matched := UsernameValidRegex.MatchString(str) - if !matched { - return xerrors.New("must be alphanumeric with hyphens") - } - return nil -} - -// TemplateVersionNameValid returns whether the input string is a valid template version name. -func TemplateVersionNameValid(str string) error { - if len(str) > 64 { - return xerrors.New("must be <= 64 characters") - } - matched := templateVersionName.MatchString(str) - if !matched { - return xerrors.New("must be alphanumeric with underscores and dots") - } - return nil -} - -// DisplayNameValid returns whether the input string is a valid template display name. -func DisplayNameValid(str string) error { - if len(str) == 0 { - return nil // empty display_name is correct - } - if len(str) > 64 { - return xerrors.New("must be <= 64 characters") - } - matched := templateDisplayName.MatchString(str) - if !matched { - return xerrors.New("must be alphanumeric with spaces") - } - return nil -} - -// UserRealNameValid returns whether the input string is a valid real user name. -func UserRealNameValid(str string) error { - if len(str) > 128 { - return xerrors.New("must be <= 128 characters") - } - - if strings.TrimSpace(str) != str { - return xerrors.New("must not have leading or trailing whitespace") - } - return nil -} - -// GroupNameValid returns whether the input string is a valid group name. -func GroupNameValid(str string) error { - // 36 is to support using UUIDs as the group name. - if len(str) > 36 { - return xerrors.New("must be <= 36 characters") - } - // Avoid conflicts with routes like /groups/new and /groups/create. - if str == "new" || str == "create" { - return xerrors.Errorf("cannot use %q as a name", str) - } - matched := UsernameValidRegex.MatchString(str) - if !matched { - return xerrors.New("must be alphanumeric with hyphens") - } - return nil -} - -// NormalizeUserRealName normalizes a user name such that it will pass -// validation by UserRealNameValid. This is done to avoid blocking -// little Bobby Whitespace from using Coder. -func NormalizeRealUsername(str string) string { - s := strings.TrimSpace(str) - if len(s) > 128 { - s = s[:128] - } - return s -} diff --git a/enterprise/cli/groupcreate.go b/enterprise/cli/groupcreate.go index 21d5535583045..4222ddffc701c 100644 --- a/enterprise/cli/groupcreate.go +++ b/enterprise/cli/groupcreate.go @@ -35,6 +35,11 @@ func (r *RootCmd) groupCreate() *serpent.Command { return xerrors.Errorf("current organization: %w", err) } + err = codersdk.GroupNameValid(inv.Args[0]) + if err != nil { + return xerrors.Errorf("group name %q is invalid: %w", inv.Args[0], err) + } + group, err := client.CreateGroup(ctx, org.ID, codersdk.CreateGroupRequest{ Name: inv.Args[0], DisplayName: displayName, @@ -61,7 +66,16 @@ func (r *RootCmd) groupCreate() *serpent.Command { Flag: "display-name", Description: `Optional human friendly name for the group.`, Env: "CODER_DISPLAY_NAME", - Value: serpent.StringOf(&displayName), + Value: serpent.Validate(serpent.StringOf(&displayName), func(_displayName *serpent.String) error { + displayName := _displayName.String() + if displayName != "" { + err := codersdk.DisplayNameValid(displayName) + if err != nil { + return xerrors.Errorf("group display name %q is invalid: %w", displayName, err) + } + } + return nil + }), }, } orgContext.AttachOptions(cmd)