mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(codersdk): export name validators (#14551)
This commit is contained in:
+11
-4
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user