feat(codersdk): export name validators (#14551)

This commit is contained in:
Ethan
2024-09-04 08:34:39 +00:00
committed by GitHub
parent 01a904c133
commit 8f85464fe6
6 changed files with 59 additions and 135 deletions
+11 -4
View File
@@ -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
+6 -1
View File
@@ -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."),
+10 -3
View File
@@ -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
+17 -1
View File
@@ -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",
-125
View File
@@ -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
}
+15 -1
View File
@@ -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)