Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 01a904c

Browse files
feat(codersdk): export name validators (#14550)
* feat(codersdk): export name validators * review
1 parent 093d243 commit 01a904c

File tree

10 files changed

+154
-31
lines changed

10 files changed

+154
-31
lines changed

cli/usercreate.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/coder/pretty"
1212

1313
"github.com/coder/coder/v2/cli/cliui"
14-
"github.com/coder/coder/v2/coderd/httpapi"
1514
"github.com/coder/coder/v2/codersdk"
1615
"github.com/coder/coder/v2/cryptorand"
1716
"github.com/coder/serpent"
@@ -72,7 +71,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
7271
if err != nil {
7372
return err
7473
}
75-
name = httpapi.NormalizeRealUsername(rawName)
74+
name = codersdk.NormalizeRealUsername(rawName)
7675
if !strings.EqualFold(rawName, name) {
7776
cliui.Warnf(inv.Stderr, "Normalized name to %q", name)
7877
}

coderd/externalauth/externalauth.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323

2424
"github.com/coder/coder/v2/coderd/database"
2525
"github.com/coder/coder/v2/coderd/database/dbtime"
26-
"github.com/coder/coder/v2/coderd/httpapi"
2726
"github.com/coder/coder/v2/coderd/promoauth"
2827
"github.com/coder/coder/v2/codersdk"
2928
"github.com/coder/retry"
@@ -486,7 +485,7 @@ func ConvertConfig(instrument *promoauth.Factory, entries []codersdk.ExternalAut
486485
// apply their client secret and ID, and have the UI appear nicely.
487486
applyDefaultsToConfig(&entry)
488487

489-
valid := httpapi.NameValid(entry.ID)
488+
valid := codersdk.NameValid(entry.ID)
490489
if valid != nil {
491490
return nil, xerrors.Errorf("external auth provider %q doesn't have a valid id: %w", entry.ID, valid)
492491
}

coderd/httpapi/httpapi.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func init() {
4343
if !ok {
4444
return false
4545
}
46-
valid := NameValid(str)
46+
valid := codersdk.NameValid(str)
4747
return valid == nil
4848
}
4949
for _, tag := range []string{"username", "organization_name", "template_name", "workspace_name", "oauth2_app_name"} {
@@ -59,7 +59,7 @@ func init() {
5959
if !ok {
6060
return false
6161
}
62-
valid := DisplayNameValid(str)
62+
valid := codersdk.DisplayNameValid(str)
6363
return valid == nil
6464
}
6565
for _, displayNameTag := range []string{"organization_display_name", "template_display_name", "group_display_name"} {
@@ -75,7 +75,7 @@ func init() {
7575
if !ok {
7676
return false
7777
}
78-
valid := TemplateVersionNameValid(str)
78+
valid := codersdk.TemplateVersionNameValid(str)
7979
return valid == nil
8080
}
8181
err := Validate.RegisterValidation("template_version_name", templateVersionNameValidator)
@@ -89,7 +89,7 @@ func init() {
8989
if !ok {
9090
return false
9191
}
92-
valid := UserRealNameValid(str)
92+
valid := codersdk.UserRealNameValid(str)
9393
return valid == nil
9494
}
9595
err = Validate.RegisterValidation("user_real_name", userRealNameValidator)
@@ -103,7 +103,7 @@ func init() {
103103
if !ok {
104104
return false
105105
}
106-
valid := GroupNameValid(str)
106+
valid := codersdk.GroupNameValid(str)
107107
return valid == nil
108108
}
109109
err = Validate.RegisterValidation("group_name", groupNameValidator)

coderd/httpapi/name.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func UsernameFrom(str string) string {
3838
}
3939

4040
// NameValid returns whether the input string is a valid name.
41-
// It is a generic validator for any name (user, workspace, template, role name, etc.).
41+
// It is a generic validator for any name that doesn't have it's own validator.
4242
func NameValid(str string) error {
4343
if len(str) > 32 {
4444
return xerrors.New("must be <= 32 characters")

coderd/userauth.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
602602
}
603603

604604
ghName := ghUser.GetName()
605-
normName := httpapi.NormalizeRealUsername(ghName)
605+
normName := codersdk.NormalizeRealUsername(ghName)
606606

607607
// If we have a nil GitHub ID, that is a big problem. That would mean we link
608608
// this user and all other users with this bug to the same uuid.
@@ -951,15 +951,15 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
951951
// The username is a required property in Coder. We make a best-effort
952952
// attempt at using what the claims provide, but if that fails we will
953953
// generate a random username.
954-
usernameValid := httpapi.NameValid(username)
954+
usernameValid := codersdk.NameValid(username)
955955
if usernameValid != nil {
956956
// If no username is provided, we can default to use the email address.
957957
// This will be converted in the from function below, so it's safe
958958
// to keep the domain.
959959
if username == "" {
960960
username = email
961961
}
962-
username = httpapi.UsernameFrom(username)
962+
username = codersdk.UsernameFrom(username)
963963
}
964964

965965
if len(api.OIDCConfig.EmailDomain) > 0 {
@@ -994,7 +994,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
994994
nameRaw, ok := mergedClaims[api.OIDCConfig.NameField]
995995
if ok {
996996
name, _ = nameRaw.(string)
997-
name = httpapi.NormalizeRealUsername(name)
997+
name = codersdk.NormalizeRealUsername(name)
998998
}
999999

10001000
var picture string
@@ -1389,7 +1389,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
13891389
for i := 0; i < 10; i++ {
13901390
alternate := fmt.Sprintf("%s-%s", original, namesgenerator.GetRandomName(1))
13911391

1392-
params.Username = httpapi.UsernameFrom(alternate)
1392+
params.Username = codersdk.UsernameFrom(alternate)
13931393

13941394
//nolint:gocritic
13951395
_, err := tx.GetUserByEmailOrUsername(dbauthz.AsSystemRestricted(ctx), database.GetUserByEmailOrUsernameParams{

coderd/users.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1287,7 +1287,7 @@ type CreateUserRequest struct {
12871287
func (api *API) CreateUser(ctx context.Context, store database.Store, req CreateUserRequest) (database.User, error) {
12881288
// Ensure the username is valid. It's the caller's responsibility to ensure
12891289
// the username is valid and unique.
1290-
if usernameValid := httpapi.NameValid(req.Username); usernameValid != nil {
1290+
if usernameValid := codersdk.NameValid(req.Username); usernameValid != nil {
12911291
return database.User{}, xerrors.Errorf("invalid username %q: %w", req.Username, usernameValid)
12921292
}
12931293

@@ -1299,7 +1299,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
12991299
ID: uuid.New(),
13001300
Email: req.Email,
13011301
Username: req.Username,
1302-
Name: httpapi.NormalizeRealUsername(req.Name),
1302+
Name: codersdk.NormalizeRealUsername(req.Name),
13031303
CreatedAt: dbtime.Now(),
13041304
UpdatedAt: dbtime.Now(),
13051305
HashedPassword: []byte{},

codersdk/name.go

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package codersdk
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/moby/moby/pkg/namesgenerator"
8+
"golang.org/x/xerrors"
9+
)
10+
11+
var (
12+
UsernameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
13+
usernameReplace = regexp.MustCompile("[^a-zA-Z0-9-]*")
14+
15+
templateVersionName = regexp.MustCompile(`^[a-zA-Z0-9]+(?:[_.-]{1}[a-zA-Z0-9]+)*$`)
16+
templateDisplayName = regexp.MustCompile(`^[^\s](.*[^\s])?$`)
17+
)
18+
19+
// UsernameFrom returns a best-effort username from the provided string.
20+
//
21+
// It first attempts to validate the incoming string, which will
22+
// be returned if it is valid. It then will attempt to extract
23+
// the username from an email address. If no success happens during
24+
// these steps, a random username will be returned.
25+
func UsernameFrom(str string) string {
26+
if valid := NameValid(str); valid == nil {
27+
return str
28+
}
29+
emailAt := strings.LastIndex(str, "@")
30+
if emailAt >= 0 {
31+
str = str[:emailAt]
32+
}
33+
str = usernameReplace.ReplaceAllString(str, "")
34+
if valid := NameValid(str); valid == nil {
35+
return str
36+
}
37+
return strings.ReplaceAll(namesgenerator.GetRandomName(1), "_", "-")
38+
}
39+
40+
// NameValid returns whether the input string is a valid name.
41+
// It is a generic validator for any name (user, workspace, template, role name, etc.).
42+
func NameValid(str string) error {
43+
if len(str) > 32 {
44+
return xerrors.New("must be <= 32 characters")
45+
}
46+
if len(str) < 1 {
47+
return xerrors.New("must be >= 1 character")
48+
}
49+
// Avoid conflicts with routes like /templates/new and /groups/create.
50+
if str == "new" || str == "create" {
51+
return xerrors.Errorf("cannot use %q as a name", str)
52+
}
53+
matched := UsernameValidRegex.MatchString(str)
54+
if !matched {
55+
return xerrors.New("must be alphanumeric with hyphens")
56+
}
57+
return nil
58+
}
59+
60+
// TemplateVersionNameValid returns whether the input string is a valid template version name.
61+
func TemplateVersionNameValid(str string) error {
62+
if len(str) > 64 {
63+
return xerrors.New("must be <= 64 characters")
64+
}
65+
matched := templateVersionName.MatchString(str)
66+
if !matched {
67+
return xerrors.New("must be alphanumeric with underscores and dots")
68+
}
69+
return nil
70+
}
71+
72+
// DisplayNameValid returns whether the input string is a valid template display name.
73+
func DisplayNameValid(str string) error {
74+
if len(str) == 0 {
75+
return nil // empty display_name is correct
76+
}
77+
if len(str) > 64 {
78+
return xerrors.New("must be <= 64 characters")
79+
}
80+
matched := templateDisplayName.MatchString(str)
81+
if !matched {
82+
return xerrors.New("must be alphanumeric with spaces")
83+
}
84+
return nil
85+
}
86+
87+
// UserRealNameValid returns whether the input string is a valid real user name.
88+
func UserRealNameValid(str string) error {
89+
if len(str) > 128 {
90+
return xerrors.New("must be <= 128 characters")
91+
}
92+
93+
if strings.TrimSpace(str) != str {
94+
return xerrors.New("must not have leading or trailing whitespace")
95+
}
96+
return nil
97+
}
98+
99+
// GroupNameValid returns whether the input string is a valid group name.
100+
func GroupNameValid(str string) error {
101+
// 36 is to support using UUIDs as the group name.
102+
if len(str) > 36 {
103+
return xerrors.New("must be <= 36 characters")
104+
}
105+
// Avoid conflicts with routes like /groups/new and /groups/create.
106+
if str == "new" || str == "create" {
107+
return xerrors.Errorf("cannot use %q as a name", str)
108+
}
109+
matched := UsernameValidRegex.MatchString(str)
110+
if !matched {
111+
return xerrors.New("must be alphanumeric with hyphens")
112+
}
113+
return nil
114+
}
115+
116+
// NormalizeUserRealName normalizes a user name such that it will pass
117+
// validation by UserRealNameValid. This is done to avoid blocking
118+
// little Bobby Whitespace from using Coder.
119+
func NormalizeRealUsername(str string) string {
120+
s := strings.TrimSpace(str)
121+
if len(s) > 128 {
122+
s = s[:128]
123+
}
124+
return s
125+
}

coderd/httpapi/name_test.go renamed to codersdk/name_test.go

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package httpapi_test
1+
package codersdk_test
22

33
import (
44
"strings"
@@ -7,7 +7,7 @@ import (
77
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
99

10-
"github.com/coder/coder/v2/coderd/httpapi"
10+
"github.com/coder/coder/v2/codersdk"
1111
"github.com/coder/coder/v2/testutil"
1212
)
1313

@@ -62,7 +62,7 @@ func TestUsernameValid(t *testing.T) {
6262
testCase := testCase
6363
t.Run(testCase.Username, func(t *testing.T) {
6464
t.Parallel()
65-
valid := httpapi.NameValid(testCase.Username)
65+
valid := codersdk.NameValid(testCase.Username)
6666
require.Equal(t, testCase.Valid, valid == nil)
6767
})
6868
}
@@ -117,7 +117,7 @@ func TestTemplateDisplayNameValid(t *testing.T) {
117117
testCase := testCase
118118
t.Run(testCase.Name, func(t *testing.T) {
119119
t.Parallel()
120-
valid := httpapi.DisplayNameValid(testCase.Name)
120+
valid := codersdk.DisplayNameValid(testCase.Name)
121121
require.Equal(t, testCase.Valid, valid == nil)
122122
})
123123
}
@@ -158,7 +158,7 @@ func TestTemplateVersionNameValid(t *testing.T) {
158158
testCase := testCase
159159
t.Run(testCase.Name, func(t *testing.T) {
160160
t.Parallel()
161-
valid := httpapi.TemplateVersionNameValid(testCase.Name)
161+
valid := codersdk.TemplateVersionNameValid(testCase.Name)
162162
require.Equal(t, testCase.Valid, valid == nil)
163163
})
164164
}
@@ -169,7 +169,7 @@ func TestGeneratedTemplateVersionNameValid(t *testing.T) {
169169

170170
for i := 0; i < 1000; i++ {
171171
name := testutil.GetRandomName(t)
172-
err := httpapi.TemplateVersionNameValid(name)
172+
err := codersdk.TemplateVersionNameValid(name)
173173
require.NoError(t, err, "invalid template version name: %s", name)
174174
}
175175
}
@@ -199,9 +199,9 @@ func TestFrom(t *testing.T) {
199199
testCase := testCase
200200
t.Run(testCase.From, func(t *testing.T) {
201201
t.Parallel()
202-
converted := httpapi.UsernameFrom(testCase.From)
202+
converted := codersdk.UsernameFrom(testCase.From)
203203
t.Log(converted)
204-
valid := httpapi.NameValid(converted)
204+
valid := codersdk.NameValid(converted)
205205
require.True(t, valid == nil)
206206
if testCase.Match == "" {
207207
require.NotEqual(t, testCase.From, converted)
@@ -245,9 +245,9 @@ func TestUserRealNameValid(t *testing.T) {
245245
testCase := testCase
246246
t.Run(testCase.Name, func(t *testing.T) {
247247
t.Parallel()
248-
err := httpapi.UserRealNameValid(testCase.Name)
249-
norm := httpapi.NormalizeRealUsername(testCase.Name)
250-
normErr := httpapi.UserRealNameValid(norm)
248+
err := codersdk.UserRealNameValid(testCase.Name)
249+
norm := codersdk.NormalizeRealUsername(testCase.Name)
250+
normErr := codersdk.UserRealNameValid(norm)
251251
assert.NoError(t, normErr)
252252
assert.Equal(t, testCase.Valid, err == nil)
253253
assert.Equal(t, testCase.Valid, norm == testCase.Name, "invalid name should be different after normalization")

enterprise/coderd/roles.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ func validOrganizationRoleRequest(ctx context.Context, req codersdk.CustomRoleRe
266266
return false
267267
}
268268

269-
if err := httpapi.NameValid(req.Name); err != nil {
269+
if err := codersdk.NameValid(req.Name); err != nil {
270270
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
271271
Message: "Invalid role name",
272272
Detail: err.Error(),

enterprise/coderd/scim.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,15 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
206206
// The username is a required property in Coder. We make a best-effort
207207
// attempt at using what the claims provide, but if that fails we will
208208
// generate a random username.
209-
usernameValid := httpapi.NameValid(sUser.UserName)
209+
usernameValid := codersdk.NameValid(sUser.UserName)
210210
if usernameValid != nil {
211211
// If no username is provided, we can default to use the email address.
212212
// This will be converted in the from function below, so it's safe
213213
// to keep the domain.
214214
if sUser.UserName == "" {
215215
sUser.UserName = email
216216
}
217-
sUser.UserName = httpapi.UsernameFrom(sUser.UserName)
217+
sUser.UserName = codersdk.UsernameFrom(sUser.UserName)
218218
}
219219

220220
// TODO: This is a temporary solution that does not support multi-org

0 commit comments

Comments
 (0)