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)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: