| @@ -57,6 +57,21 @@ func (err ErrNamePatternNotAllowed) Error() string { | |||||
| return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern) | return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern) | ||||
| } | } | ||||
| // ErrNameCharsNotAllowed represents a "character not allowed in name" error. | |||||
| type ErrNameCharsNotAllowed struct { | |||||
| Name string | |||||
| } | |||||
| // IsErrNameCharsNotAllowed checks if an error is an ErrNameCharsNotAllowed. | |||||
| func IsErrNameCharsNotAllowed(err error) bool { | |||||
| _, ok := err.(ErrNameCharsNotAllowed) | |||||
| return ok | |||||
| } | |||||
| func (err ErrNameCharsNotAllowed) Error() string { | |||||
| return fmt.Sprintf("User name is invalid [%s]: must be valid alpha or numeric or dash(-_) or dot characters", err.Name) | |||||
| } | |||||
| // ErrSSHDisabled represents an "SSH disabled" error. | // ErrSSHDisabled represents an "SSH disabled" error. | ||||
| type ErrSSHDisabled struct { | type ErrSSHDisabled struct { | ||||
| } | } | ||||
| @@ -12,7 +12,6 @@ import ( | |||||
| "fmt" | "fmt" | ||||
| "net/smtp" | "net/smtp" | ||||
| "net/textproto" | "net/textproto" | ||||
| "regexp" | |||||
| "strings" | "strings" | ||||
| "code.gitea.io/gitea/modules/auth/ldap" | "code.gitea.io/gitea/modules/auth/ldap" | ||||
| @@ -455,10 +454,6 @@ func composeFullName(firstname, surname, username string) string { | |||||
| } | } | ||||
| } | } | ||||
| var ( | |||||
| alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) | |||||
| ) | |||||
| // LoginViaLDAP queries if login/password is valid against the LDAP directory pool, | // LoginViaLDAP queries if login/password is valid against the LDAP directory pool, | ||||
| // and create a local user if success when enabled. | // and create a local user if success when enabled. | ||||
| func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) { | func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*User, error) { | ||||
| @@ -503,10 +498,6 @@ func LoginViaLDAP(user *User, login, password string, source *LoginSource) (*Use | |||||
| if len(sr.Username) == 0 { | if len(sr.Username) == 0 { | ||||
| sr.Username = login | sr.Username = login | ||||
| } | } | ||||
| // Validate username make sure it satisfies requirement. | |||||
| if alphaDashDotPattern.MatchString(sr.Username) { | |||||
| return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", sr.Username) | |||||
| } | |||||
| if len(sr.Mail) == 0 { | if len(sr.Mail) == 0 { | ||||
| sr.Mail = fmt.Sprintf("%s@localhost", sr.Username) | sr.Mail = fmt.Sprintf("%s@localhost", sr.Username) | ||||
| @@ -666,7 +657,8 @@ func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPC | |||||
| // LoginViaPAM queries if login/password is valid against the PAM, | // LoginViaPAM queries if login/password is valid against the PAM, | ||||
| // and create a local user if success when enabled. | // and create a local user if success when enabled. | ||||
| func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) { | func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig) (*User, error) { | ||||
| if err := pam.Auth(cfg.ServiceName, login, password); err != nil { | |||||
| pamLogin, err := pam.Auth(cfg.ServiceName, login, password) | |||||
| if err != nil { | |||||
| if strings.Contains(err.Error(), "Authentication failure") { | if strings.Contains(err.Error(), "Authentication failure") { | ||||
| return nil, ErrUserNotExist{0, login, 0} | return nil, ErrUserNotExist{0, login, 0} | ||||
| } | } | ||||
| @@ -677,14 +669,21 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon | |||||
| return user, nil | return user, nil | ||||
| } | } | ||||
| // Allow PAM sources with `@` in their name, like from Active Directory | |||||
| username := pamLogin | |||||
| idx := strings.Index(pamLogin, "@") | |||||
| if idx > -1 { | |||||
| username = pamLogin[:idx] | |||||
| } | |||||
| user = &User{ | user = &User{ | ||||
| LowerName: strings.ToLower(login), | |||||
| Name: login, | |||||
| Email: login, | |||||
| LowerName: strings.ToLower(username), | |||||
| Name: username, | |||||
| Email: pamLogin, | |||||
| Passwd: password, | Passwd: password, | ||||
| LoginType: LoginPAM, | LoginType: LoginPAM, | ||||
| LoginSource: sourceID, | LoginSource: sourceID, | ||||
| LoginName: login, | |||||
| LoginName: login, // This is what the user typed in | |||||
| IsActive: true, | IsActive: true, | ||||
| } | } | ||||
| return user, CreateUser(user) | return user, CreateUser(user) | ||||
| @@ -18,6 +18,7 @@ import ( | |||||
| "image/png" | "image/png" | ||||
| "os" | "os" | ||||
| "path/filepath" | "path/filepath" | ||||
| "regexp" | |||||
| "strconv" | "strconv" | ||||
| "strings" | "strings" | ||||
| "time" | "time" | ||||
| @@ -87,6 +88,9 @@ var ( | |||||
| // ErrUnsupportedLoginType login source is unknown error | // ErrUnsupportedLoginType login source is unknown error | ||||
| ErrUnsupportedLoginType = errors.New("Login source is unknown") | ErrUnsupportedLoginType = errors.New("Login source is unknown") | ||||
| // Characters prohibited in a user name (anything except A-Za-z0-9_.-) | |||||
| alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) | |||||
| ) | ) | ||||
| // User represents the object of individual and member of organization. | // User represents the object of individual and member of organization. | ||||
| @@ -906,6 +910,11 @@ func isUsableName(names, patterns []string, name string) error { | |||||
| // IsUsableUsername returns an error when a username is reserved | // IsUsableUsername returns an error when a username is reserved | ||||
| func IsUsableUsername(name string) error { | func IsUsableUsername(name string) error { | ||||
| // Validate username make sure it satisfies requirement. | |||||
| if alphaDashDotPattern.MatchString(name) { | |||||
| // Note: usually this error is normally caught up earlier in the UI | |||||
| return ErrNameCharsNotAllowed{Name: name} | |||||
| } | |||||
| return isUsableName(reservedUsernames, reservedUserPatterns, name) | return isUsableName(reservedUsernames, reservedUserPatterns, name) | ||||
| } | } | ||||
| @@ -13,7 +13,7 @@ import ( | |||||
| ) | ) | ||||
| // Auth pam auth service | // Auth pam auth service | ||||
| func Auth(serviceName, userName, passwd string) error { | |||||
| func Auth(serviceName, userName, passwd string) (string, error) { | |||||
| t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) { | t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) { | ||||
| switch s { | switch s { | ||||
| case pam.PromptEchoOff: | case pam.PromptEchoOff: | ||||
| @@ -25,12 +25,14 @@ func Auth(serviceName, userName, passwd string) error { | |||||
| }) | }) | ||||
| if err != nil { | if err != nil { | ||||
| return err | |||||
| return "", err | |||||
| } | } | ||||
| if err = t.Authenticate(0); err != nil { | if err = t.Authenticate(0); err != nil { | ||||
| return err | |||||
| return "", err | |||||
| } | } | ||||
| return nil | |||||
| // PAM login names might suffer transformations in the PAM stack. | |||||
| // We should take whatever the PAM stack returns for it. | |||||
| return t.GetItem(pam.User) | |||||
| } | } | ||||
| @@ -11,6 +11,6 @@ import ( | |||||
| ) | ) | ||||
| // Auth not supported lack of pam tag | // Auth not supported lack of pam tag | ||||
| func Auth(serviceName, userName, passwd string) error { | |||||
| return errors.New("PAM not supported") | |||||
| func Auth(serviceName, userName, passwd string) (string, error) { | |||||
| return "", errors.New("PAM not supported") | |||||
| } | } | ||||
| @@ -35,7 +35,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { | |||||
| // Create org. | // Create org. | ||||
| org := &models.User{ | org := &models.User{ | ||||
| Name: "All repo", | |||||
| Name: "All_repo", | |||||
| IsActive: true, | IsActive: true, | ||||
| Type: models.UserTypeOrganization, | Type: models.UserTypeOrganization, | ||||
| Visibility: structs.VisibleTypePublic, | Visibility: structs.VisibleTypePublic, | ||||
| @@ -379,6 +379,7 @@ user_bio = Biography | |||||
| form.name_reserved = The username '%s' is reserved. | form.name_reserved = The username '%s' is reserved. | ||||
| form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username. | form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username. | ||||
| form.name_chars_not_allowed = User name '%s' contains invalid characters. | |||||
| [settings] | [settings] | ||||
| profile = Profile | profile = Profile | ||||
| @@ -124,6 +124,9 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) { | |||||
| case models.IsErrNamePatternNotAllowed(err): | case models.IsErrNamePatternNotAllowed(err): | ||||
| ctx.Data["Err_UserName"] = true | ctx.Data["Err_UserName"] = true | ||||
| ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) | ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form) | ||||
| case models.IsErrNameCharsNotAllowed(err): | |||||
| ctx.Data["Err_UserName"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplUserNew, &form) | |||||
| default: | default: | ||||
| ctx.ServerError("CreateUser", err) | ctx.ServerError("CreateUser", err) | ||||
| } | } | ||||
| @@ -67,6 +67,7 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) { | |||||
| if err := models.CreateOrganization(org, u); err != nil { | if err := models.CreateOrganization(org, u); err != nil { | ||||
| if models.IsErrUserAlreadyExist(err) || | if models.IsErrUserAlreadyExist(err) || | ||||
| models.IsErrNameReserved(err) || | models.IsErrNameReserved(err) || | ||||
| models.IsErrNameCharsNotAllowed(err) || | |||||
| models.IsErrNamePatternNotAllowed(err) { | models.IsErrNamePatternNotAllowed(err) { | ||||
| ctx.Error(http.StatusUnprocessableEntity, "", err) | ctx.Error(http.StatusUnprocessableEntity, "", err) | ||||
| } else { | } else { | ||||
| @@ -91,6 +91,7 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) { | |||||
| if models.IsErrUserAlreadyExist(err) || | if models.IsErrUserAlreadyExist(err) || | ||||
| models.IsErrEmailAlreadyUsed(err) || | models.IsErrEmailAlreadyUsed(err) || | ||||
| models.IsErrNameReserved(err) || | models.IsErrNameReserved(err) || | ||||
| models.IsErrNameCharsNotAllowed(err) || | |||||
| models.IsErrNamePatternNotAllowed(err) { | models.IsErrNamePatternNotAllowed(err) { | ||||
| ctx.Error(http.StatusUnprocessableEntity, "", err) | ctx.Error(http.StatusUnprocessableEntity, "", err) | ||||
| } else { | } else { | ||||
| @@ -179,6 +179,7 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { | |||||
| if err := models.CreateOrganization(org, ctx.User); err != nil { | if err := models.CreateOrganization(org, ctx.User); err != nil { | ||||
| if models.IsErrUserAlreadyExist(err) || | if models.IsErrUserAlreadyExist(err) || | ||||
| models.IsErrNameReserved(err) || | models.IsErrNameReserved(err) || | ||||
| models.IsErrNameCharsNotAllowed(err) || | |||||
| models.IsErrNamePatternNotAllowed(err) { | models.IsErrNamePatternNotAllowed(err) { | ||||
| ctx.Error(http.StatusUnprocessableEntity, "", err) | ctx.Error(http.StatusUnprocessableEntity, "", err) | ||||
| } else { | } else { | ||||
| @@ -199,6 +199,8 @@ func handleMigrateError(ctx *context.APIContext, repoOwner *models.User, remoteA | |||||
| ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) | ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("You have already reached your limit of %d repositories.", repoOwner.MaxCreationLimit())) | ||||
| case models.IsErrNameReserved(err): | case models.IsErrNameReserved(err): | ||||
| ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name)) | ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' is reserved.", err.(models.ErrNameReserved).Name)) | ||||
| case models.IsErrNameCharsNotAllowed(err): | |||||
| ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The username '%s' contains invalid characters.", err.(models.ErrNameCharsNotAllowed).Name)) | |||||
| case models.IsErrNamePatternNotAllowed(err): | case models.IsErrNamePatternNotAllowed(err): | ||||
| ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern)) | ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("The pattern '%s' is not allowed in a username.", err.(models.ErrNamePatternNotAllowed).Pattern)) | ||||
| default: | default: | ||||
| @@ -928,6 +928,7 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au | |||||
| LoginName: gothUser.(goth.User).UserID, | LoginName: gothUser.(goth.User).UserID, | ||||
| } | } | ||||
| //nolint: dupl | |||||
| if err := models.CreateUser(u); err != nil { | if err := models.CreateUser(u); err != nil { | ||||
| switch { | switch { | ||||
| case models.IsErrUserAlreadyExist(err): | case models.IsErrUserAlreadyExist(err): | ||||
| @@ -942,6 +943,9 @@ func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form au | |||||
| case models.IsErrNamePatternNotAllowed(err): | case models.IsErrNamePatternNotAllowed(err): | ||||
| ctx.Data["Err_UserName"] = true | ctx.Data["Err_UserName"] = true | ||||
| ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form) | ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplLinkAccount, &form) | ||||
| case models.IsErrNameCharsNotAllowed(err): | |||||
| ctx.Data["Err_UserName"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplLinkAccount, &form) | |||||
| default: | default: | ||||
| ctx.ServerError("CreateUser", err) | ctx.ServerError("CreateUser", err) | ||||
| } | } | ||||
| @@ -400,6 +400,7 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si | |||||
| Passwd: password, | Passwd: password, | ||||
| IsActive: !setting.Service.RegisterEmailConfirm, | IsActive: !setting.Service.RegisterEmailConfirm, | ||||
| } | } | ||||
| //nolint: dupl | |||||
| if err := models.CreateUser(u); err != nil { | if err := models.CreateUser(u); err != nil { | ||||
| switch { | switch { | ||||
| case models.IsErrUserAlreadyExist(err): | case models.IsErrUserAlreadyExist(err): | ||||
| @@ -414,6 +415,9 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si | |||||
| case models.IsErrNamePatternNotAllowed(err): | case models.IsErrNamePatternNotAllowed(err): | ||||
| ctx.Data["Err_UserName"] = true | ctx.Data["Err_UserName"] = true | ||||
| ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUpOID, &form) | ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(models.ErrNamePatternNotAllowed).Pattern), tplSignUpOID, &form) | ||||
| case models.IsErrNameCharsNotAllowed(err): | |||||
| ctx.Data["Err_UserName"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(models.ErrNameCharsNotAllowed).Name), tplSignUpOID, &form) | |||||
| default: | default: | ||||
| ctx.ServerError("CreateUser", err) | ctx.ServerError("CreateUser", err) | ||||
| } | } | ||||
| @@ -58,6 +58,9 @@ func handleUsernameChange(ctx *context.Context, newName string) { | |||||
| case models.IsErrNamePatternNotAllowed(err): | case models.IsErrNamePatternNotAllowed(err): | ||||
| ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) | ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) | ||||
| ctx.Redirect(setting.AppSubURL + "/user/settings") | ctx.Redirect(setting.AppSubURL + "/user/settings") | ||||
| case models.IsErrNameCharsNotAllowed(err): | |||||
| ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName)) | |||||
| ctx.Redirect(setting.AppSubURL + "/user/settings") | |||||
| default: | default: | ||||
| ctx.ServerError("ChangeUserName", err) | ctx.ServerError("ChangeUserName", err) | ||||
| } | } | ||||