diff --git a/go.mod b/go.mod index 11b03fab82ca6..f82fbef203525 100644 --- a/go.mod +++ b/go.mod @@ -235,6 +235,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mozillazg/go-unidecode v0.2.0 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/nwaples/rardecode v1.1.3 // indirect diff --git a/go.sum b/go.sum index 5bf1d2d92c55f..77e3b05516227 100644 --- a/go.sum +++ b/go.sum @@ -829,6 +829,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mozillazg/go-unidecode v0.2.0 h1:vFGEzAH9KSwyWmXCOblazEWDh7fOkpmy/Z4ArmamSUc= +github.com/mozillazg/go-unidecode v0.2.0/go.mod h1:zB48+/Z5toiRolOZy9ksLryJ976VIwmDmpQ2quyt1aA= github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM= github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= diff --git a/services/auth/source/oauth2/blenderid/blenderid.go b/services/auth/source/oauth2/blenderid/blenderid.go index 364f6d35b759f..671e5e4f541a5 100644 --- a/services/auth/source/oauth2/blenderid/blenderid.go +++ b/services/auth/source/oauth2/blenderid/blenderid.go @@ -164,7 +164,7 @@ func userFromReader(r io.Reader, user *goth.User) error { } user.Email = u.Email user.Name = u.Name - user.NickName = u.NickName + user.NickName = gitealizeUsername(u.NickName) user.UserID = strconv.Itoa(u.ID) user.AvatarURL = fmt.Sprintf("https://id.blender.org/api/user/%s/avatar", user.UserID) return nil diff --git a/services/auth/source/oauth2/blenderid/gitealize_usernames.go b/services/auth/source/oauth2/blenderid/gitealize_usernames.go new file mode 100644 index 0000000000000..62fa556cf3367 --- /dev/null +++ b/services/auth/source/oauth2/blenderid/gitealize_usernames.go @@ -0,0 +1,64 @@ +package blenderid + +// SPDX-License-Identifier: MIT + +import ( + "regexp" + "strings" + + "code.gitea.io/gitea/models/user" + "github.com/mozillazg/go-unidecode" +) + +var ( + reInvalidCharsPattern = regexp.MustCompile(`[^\da-zA-Z.\w-]+`) + + // Consecutive non-alphanumeric at start: + reConsPrefix = regexp.MustCompile(`^[._-]+`) + reConsSuffix = regexp.MustCompile(`[._-]+$`) + reConsInfix = regexp.MustCompile(`[._-]{2,}`) +) + +// gitealizeUsername turns a valid Blender ID nickname into a valid Gitea username. +func gitealizeUsername(bidNickname string) string { + // Remove accents and other non-ASCIIness. + asciiUsername := unidecode.Unidecode(bidNickname) + asciiUsername = strings.TrimSpace(asciiUsername) + asciiUsername = strings.ReplaceAll(asciiUsername, " ", "_") + + err := user.IsUsableUsername(asciiUsername) + if err == nil && len(asciiUsername) <= 40 { + return asciiUsername + } + + newUsername := asciiUsername + newUsername = reInvalidCharsPattern.ReplaceAllString(newUsername, "_") + newUsername = reConsPrefix.ReplaceAllString(newUsername, "") + newUsername = reConsSuffix.ReplaceAllString(newUsername, "") + newUsername = reConsInfix.ReplaceAllStringFunc( + newUsername, + func(match string) string { + firstRune := []rune(match)[0] + return string(firstRune) + }) + + if newUsername == "" { + // Everything was stripped and nothing was left. Better to keep as-is and + // just let Gitea bork on it. + return asciiUsername + } + + // This includes a test for reserved names, which are easily circumvented by + // appending another character. + if user.IsUsableUsername(newUsername) != nil { + if len(newUsername) > 39 { + return newUsername[:39] + "2" + } + return newUsername + "2" + } + + if len(newUsername) > 40 { + return newUsername[:40] + } + return newUsername +} diff --git a/services/auth/source/oauth2/blenderid/gitealize_usernames_test.go b/services/auth/source/oauth2/blenderid/gitealize_usernames_test.go new file mode 100644 index 0000000000000..8a7367bf8c879 --- /dev/null +++ b/services/auth/source/oauth2/blenderid/gitealize_usernames_test.go @@ -0,0 +1,41 @@ +package blenderid + +import "testing" + +func Test_gitealizeUsername(t *testing.T) { + tests := []struct { + name string + bidNickname string + want string + }{ + {"empty", "", ""}, + {"underscore", "_", "_"}, + {"reserved-name", "ghost", "ghost2"}, // Reserved name in Gitea. + {"short", "x", "x"}, + {"simple", "simple", "simple"}, + {"start-bad", "____startbad", "startbad"}, + {"end-bad", "endbad___", "endbad"}, + {"mid-bad-1", "mid__bad", "mid_bad"}, + {"mid-bad-2", "user_.-name", "user_name"}, + {"plus-mid-single", "RT2+356", "RT2_356"}, + {"plus-mid-many", "RT2+++356", "RT2_356"}, + {"plus-end", "RT2356+", "RT2356"}, + { + "too-long", // # Max username length is 40: + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + {"accented-latin", "Ümlaut-Đenja", "Umlaut-Denja"}, + {"thai", "แบบไทย", "aebbaithy"}, + {"mandarin", "普通话", "Pu_Tong_Hua"}, + {"cyrillic", "ћирилица", "tshirilitsa"}, + {"all-bad", "------", "------"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := gitealizeUsername(tt.bidNickname); got != tt.want { + t.Errorf("gitealizeUsername() = %v, want %v", got, tt.want) + } + }) + } +}