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

* feat(codersdk): export name validators

* review
This commit is contained in:
Ethan
2024-09-04 18:17:53 +10:00
committed by GitHub
parent 093d243811
commit 01a904c133
10 changed files with 154 additions and 31 deletions

View File

@ -23,7 +23,6 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/promoauth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/retry"
@ -486,7 +485,7 @@ func ConvertConfig(instrument *promoauth.Factory, entries []codersdk.ExternalAut
// apply their client secret and ID, and have the UI appear nicely.
applyDefaultsToConfig(&entry)
valid := httpapi.NameValid(entry.ID)
valid := codersdk.NameValid(entry.ID)
if valid != nil {
return nil, xerrors.Errorf("external auth provider %q doesn't have a valid id: %w", entry.ID, valid)
}

View File

@ -43,7 +43,7 @@ func init() {
if !ok {
return false
}
valid := NameValid(str)
valid := codersdk.NameValid(str)
return valid == nil
}
for _, tag := range []string{"username", "organization_name", "template_name", "workspace_name", "oauth2_app_name"} {
@ -59,7 +59,7 @@ func init() {
if !ok {
return false
}
valid := DisplayNameValid(str)
valid := codersdk.DisplayNameValid(str)
return valid == nil
}
for _, displayNameTag := range []string{"organization_display_name", "template_display_name", "group_display_name"} {
@ -75,7 +75,7 @@ func init() {
if !ok {
return false
}
valid := TemplateVersionNameValid(str)
valid := codersdk.TemplateVersionNameValid(str)
return valid == nil
}
err := Validate.RegisterValidation("template_version_name", templateVersionNameValidator)
@ -89,7 +89,7 @@ func init() {
if !ok {
return false
}
valid := UserRealNameValid(str)
valid := codersdk.UserRealNameValid(str)
return valid == nil
}
err = Validate.RegisterValidation("user_real_name", userRealNameValidator)
@ -103,7 +103,7 @@ func init() {
if !ok {
return false
}
valid := GroupNameValid(str)
valid := codersdk.GroupNameValid(str)
return valid == nil
}
err = Validate.RegisterValidation("group_name", groupNameValidator)

View File

@ -38,7 +38,7 @@ func UsernameFrom(str string) string {
}
// NameValid returns whether the input string is a valid name.
// It is a generic validator for any name (user, workspace, template, role name, etc.).
// 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")

View File

@ -1,256 +0,0 @@
package httpapi_test
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/testutil"
)
func TestUsernameValid(t *testing.T) {
t.Parallel()
// Tests whether usernames are valid or not.
testCases := []struct {
Username string
Valid bool
}{
{"1", true},
{"12", true},
{"123", true},
{"12345678901234567890", true},
{"123456789012345678901", true},
{"a", true},
{"a1", true},
{"a1b2", true},
{"a1b2c3d4e5f6g7h8i9j0", true},
{"a1b2c3d4e5f6g7h8i9j0k", true},
{"aa", true},
{"abc", true},
{"abcdefghijklmnopqrst", true},
{"abcdefghijklmnopqrstu", true},
{"wow-test", true},
{"", false},
{" ", false},
{" a", false},
{" a ", false},
{" 1", false},
{"1 ", false},
{" aa", false},
{"aa ", false},
{" 12", false},
{"12 ", false},
{" a1", false},
{"a1 ", false},
{" abcdefghijklmnopqrstu", false},
{"abcdefghijklmnopqrstu ", false},
{" 123456789012345678901", false},
{" a1b2c3d4e5f6g7h8i9j0k", false},
{"a1b2c3d4e5f6g7h8i9j0k ", false},
{"bananas_wow", false},
{"test--now", false},
{"123456789012345678901234567890123", false},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false},
{"123456789012345678901234567890123123456789012345678901234567890123", false},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Username, func(t *testing.T) {
t.Parallel()
valid := httpapi.NameValid(testCase.Username)
require.Equal(t, testCase.Valid, valid == nil)
})
}
}
func TestTemplateDisplayNameValid(t *testing.T) {
t.Parallel()
// Tests whether display names are valid.
testCases := []struct {
Name string
Valid bool
}{
{"", true},
{"1", true},
{"12", true},
{"1 2", true},
{"123 456", true},
{"1234 678901234567890", true},
{"<b> </b>", true},
{"S", true},
{"a1", true},
{"a1K2", true},
{"!!!!1 ?????", true},
{"k\r\rm", true},
{"abcdefghijklmnopqrst", true},
{"Wow Test", true},
{"abcdefghijklmnopqrstu-", true},
{"a1b2c3d4e5f6g7h8i9j0k-", true},
{"BANANAS_wow", true},
{"test--now", true},
{"123456789012345678901234567890123", true},
{"1234567890123456789012345678901234567890123456789012345678901234", true},
{"-a1b2c3d4e5f6g7h8i9j0k", true},
{" ", false},
{"\t", false},
{"\r\r", false},
{"\t1 ", false},
{" a", false},
{"\ra ", false},
{" 1", false},
{"1 ", false},
{" aa", false},
{"aa\r", false},
{" 12", false},
{"12 ", false},
{"\fa1", false},
{"a1\t", false},
{"12345678901234567890123456789012345678901234567890123456789012345", false},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
valid := httpapi.DisplayNameValid(testCase.Name)
require.Equal(t, testCase.Valid, valid == nil)
})
}
}
func TestTemplateVersionNameValid(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
Valid bool
}{
{"1", true},
{"12", true},
{"1_2", true},
{"1-2", true},
{"cray", true},
{"123_456", true},
{"123-456", true},
{"1234_678901234567890", true},
{"1234-678901234567890", true},
{"S", true},
{"a1", true},
{"a1K2", true},
{"fuzzy_bear3", true},
{"fuzzy-bear3", true},
{"v1.0.0", true},
{"heuristic_cray2", true},
{"", false},
{".v1", false},
{"v1..0", false},
{"4--4", false},
{"<b> </b>", false},
{"!!!!1 ?????", false},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
valid := httpapi.TemplateVersionNameValid(testCase.Name)
require.Equal(t, testCase.Valid, valid == nil)
})
}
}
func TestGeneratedTemplateVersionNameValid(t *testing.T) {
t.Parallel()
for i := 0; i < 1000; i++ {
name := testutil.GetRandomName(t)
err := httpapi.TemplateVersionNameValid(name)
require.NoError(t, err, "invalid template version name: %s", name)
}
}
func TestFrom(t *testing.T) {
t.Parallel()
testCases := []struct {
From string
Match string
}{
{"1", "1"},
{"kyle@kwc.io", "kyle"},
{"kyle+wow@kwc.io", "kylewow"},
{"kyle+testing", "kyletesting"},
{"kyle-testing", "kyle-testing"},
{"much.”more unusual”@example.com", "muchmoreunusual"},
// Cases where an invalid string is provided, and the result is a random name.
{"123456789012345678901234567890123", ""},
{"very.unusual.”@”.unusual.com@example.com", ""},
{"___@ok.com", ""},
{" something with spaces ", ""},
{"--test--", ""},
{"", ""},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.From, func(t *testing.T) {
t.Parallel()
converted := httpapi.UsernameFrom(testCase.From)
t.Log(converted)
valid := httpapi.NameValid(converted)
require.True(t, valid == nil)
if testCase.Match == "" {
require.NotEqual(t, testCase.From, converted)
} else {
require.Equal(t, testCase.Match, converted)
}
})
}
}
func TestUserRealNameValid(t *testing.T) {
t.Parallel()
testCases := []struct {
Name string
Valid bool
}{
{"", true},
{" a", false},
{"a ", false},
{" a ", false},
{"1", true},
{"A", true},
{"A1", true},
{".", true},
{"Mr Bean", true},
{"Severus Snape", true},
{"Prof. Albus Percival Wulfric Brian Dumbledore", true},
{"Pablo Diego José Francisco de Paula Juan Nepomuceno María de los Remedios Cipriano de la Santísima Trinidad Ruiz y Picasso", true},
{"Hector Ó hEochagáin", true},
{"Małgorzata Kalinowska-Iszkowska", true},
{"成龍", true},
{". .", true},
{"Lord Voldemort ", false},
{" Bellatrix Lestrange", false},
{" ", false},
{strings.Repeat("a", 128), true},
{strings.Repeat("a", 129), false},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
err := httpapi.UserRealNameValid(testCase.Name)
norm := httpapi.NormalizeRealUsername(testCase.Name)
normErr := httpapi.UserRealNameValid(norm)
assert.NoError(t, normErr)
assert.Equal(t, testCase.Valid, err == nil)
assert.Equal(t, testCase.Valid, norm == testCase.Name, "invalid name should be different after normalization")
})
}
}

View File

@ -602,7 +602,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
}
ghName := ghUser.GetName()
normName := httpapi.NormalizeRealUsername(ghName)
normName := codersdk.NormalizeRealUsername(ghName)
// If we have a nil GitHub ID, that is a big problem. That would mean we link
// this user and all other users with this bug to the same uuid.
@ -951,7 +951,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
// The username is a required property in Coder. We make a best-effort
// attempt at using what the claims provide, but if that fails we will
// generate a random username.
usernameValid := httpapi.NameValid(username)
usernameValid := codersdk.NameValid(username)
if usernameValid != nil {
// If no username is provided, we can default to use the email address.
// This will be converted in the from function below, so it's safe
@ -959,7 +959,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
if username == "" {
username = email
}
username = httpapi.UsernameFrom(username)
username = codersdk.UsernameFrom(username)
}
if len(api.OIDCConfig.EmailDomain) > 0 {
@ -994,7 +994,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
nameRaw, ok := mergedClaims[api.OIDCConfig.NameField]
if ok {
name, _ = nameRaw.(string)
name = httpapi.NormalizeRealUsername(name)
name = codersdk.NormalizeRealUsername(name)
}
var picture string
@ -1389,7 +1389,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
for i := 0; i < 10; i++ {
alternate := fmt.Sprintf("%s-%s", original, namesgenerator.GetRandomName(1))
params.Username = httpapi.UsernameFrom(alternate)
params.Username = codersdk.UsernameFrom(alternate)
//nolint:gocritic
_, err := tx.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{

View File

@ -1287,7 +1287,7 @@ type CreateUserRequest struct {
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
// Ensure the username is valid. It's the caller's responsibility to ensure
// the username is valid and unique.
if usernameValid := httpapi.NameValid(req.Username); usernameValid != nil {
if usernameValid := codersdk.NameValid(req.Username); usernameValid != nil {
return database.User{}, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
}
@ -1299,7 +1299,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
ID: uuid.New(),
Email: req.Email,
Username: req.Username,
Name: httpapi.NormalizeRealUsername(req.Name),
Name: codersdk.NormalizeRealUsername(req.Name),
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
HashedPassword: []byte{},