From 2cb66fff60c95efbd58b797f1197f2421f4687ce Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 22 May 2023 08:05:44 +0800 Subject: [PATCH] Support wildcard in email domain allow/block list (#24831) Replace #20257 (which is stale and incomplete) Close #20255 Major changes: * Deprecate the "WHITELIST", use "ALLOWLIST" * Add wildcard support for EMAIL_DOMAIN_ALLOWLIST/EMAIL_DOMAIN_BLOCKLIST * Update example config file and document * Improve tests --- custom/conf/app.example.ini | 8 +-- .../config-cheat-sheet.en-us.md | 5 +- modules/setting/service.go | 27 ++++++++-- modules/setting/service_test.go | 46 +++++++++++++++++ services/forms/user_form.go | 17 ++++--- services/forms/user_form_test.go | 49 +++++++++++++------ 6 files changed, 118 insertions(+), 34 deletions(-) create mode 100644 modules/setting/service_test.go diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 27e56542e..3ee2270d3 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -700,11 +700,11 @@ LEVEL = Info ;; Whether a new user needs to be confirmed manually after registration. (Requires `REGISTER_EMAIL_CONFIRM` to be disabled.) ;REGISTER_MANUAL_CONFIRM = false ;; -;; List of domain names that are allowed to be used to register on a Gitea instance -;; gitea.io,example.com -;EMAIL_DOMAIN_WHITELIST = +;; List of domain names that are allowed to be used to register on a Gitea instance, wildcard is supported +;; eg: gitea.io,example.com,*.mydomain.com +;EMAIL_DOMAIN_ALLOWLIST = ;; -;; Comma-separated list of domain names that are not allowed to be used to register on a Gitea instance +;; Comma-separated list of domain names that are not allowed to be used to register on a Gitea instance, wildcard is supported ;EMAIL_DOMAIN_BLOCKLIST = ;; ;; Disallow registration, only allow admins to create accounts. diff --git a/docs/content/doc/administration/config-cheat-sheet.en-us.md b/docs/content/doc/administration/config-cheat-sheet.en-us.md index 5fa4c5624..cb75fc588 100644 --- a/docs/content/doc/administration/config-cheat-sheet.en-us.md +++ b/docs/content/doc/administration/config-cheat-sheet.en-us.md @@ -651,9 +651,8 @@ And the following unique queues: - `ENABLE_TIMETRACKING`: **true**: Enable Timetracking feature. - `DEFAULT_ENABLE_TIMETRACKING`: **true**: Allow repositories to use timetracking by default. - `DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME`: **true**: Only allow users with write permissions to track time. -- `EMAIL_DOMAIN_WHITELIST`: **\**: If non-empty, list of domain names that can only be used to register - on this instance. -- `EMAIL_DOMAIN_BLOCKLIST`: **\**: If non-empty, list of domain names that cannot be used to register on this instance +- `EMAIL_DOMAIN_ALLOWLIST`: **\**: If non-empty, comma separated list of domain names that can only be used to register on this instance, wildcard is supported. +- `EMAIL_DOMAIN_BLOCKLIST`: **\**: If non-empty, comma separated list of domain names that cannot be used to register on this instance, wildcard is supported. - `SHOW_REGISTRATION_BUTTON`: **! DISABLE\_REGISTRATION**: Show Registration Button - `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created diff --git a/modules/setting/service.go b/modules/setting/service.go index d4a31ba5d..03225f566 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/structs" + + "github.com/gobwas/glob" ) // enumerates all the types of captchas @@ -33,8 +35,8 @@ var Service = struct { ResetPwdCodeLives int RegisterEmailConfirm bool RegisterManualConfirm bool - EmailDomainWhitelist []string - EmailDomainBlocklist []string + EmailDomainAllowList []glob.Glob + EmailDomainBlockList []glob.Glob DisableRegistration bool AllowOnlyInternalRegistration bool AllowOnlyExternalRegistration bool @@ -114,6 +116,20 @@ func (a AllowedVisibility) ToVisibleTypeSlice() (result []structs.VisibleType) { return result } +func CompileEmailGlobList(sec ConfigSection, keys ...string) (globs []glob.Glob) { + for _, key := range keys { + list := sec.Key(key).Strings(",") + for _, s := range list { + if g, err := glob.Compile(s); err == nil { + globs = append(globs, g) + } else { + log.Error("Skip invalid email allow/block list expression %q: %v", s, err) + } + } + } + return globs +} + func loadServiceFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("service") Service.ActiveCodeLives = sec.Key("ACTIVE_CODE_LIVE_MINUTES").MustInt(180) @@ -130,8 +146,11 @@ func loadServiceFrom(rootCfg ConfigProvider) { } else { Service.RegisterManualConfirm = false } - Service.EmailDomainWhitelist = sec.Key("EMAIL_DOMAIN_WHITELIST").Strings(",") - Service.EmailDomainBlocklist = sec.Key("EMAIL_DOMAIN_BLOCKLIST").Strings(",") + if sec.HasKey("EMAIL_DOMAIN_WHITELIST") { + deprecatedSetting(rootCfg, "service", "EMAIL_DOMAIN_WHITELIST", "service", "EMAIL_DOMAIN_ALLOWLIST", "1.21") + } + Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST") + Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST") Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration)) Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true) Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() diff --git a/modules/setting/service_test.go b/modules/setting/service_test.go new file mode 100644 index 000000000..656e759f4 --- /dev/null +++ b/modules/setting/service_test.go @@ -0,0 +1,46 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "github.com/gobwas/glob" + "github.com/stretchr/testify/assert" +) + +func TestLoadServices(t *testing.T) { + oldService := Service + defer func() { + Service = oldService + }() + + cfg, err := NewConfigProviderFromData(` +[service] +EMAIL_DOMAIN_WHITELIST = d1, *.w +EMAIL_DOMAIN_ALLOWLIST = d2, *.a +EMAIL_DOMAIN_BLOCKLIST = d3, *.b +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + + match := func(globs []glob.Glob, s string) bool { + for _, g := range globs { + if g.Match(s) { + return true + } + } + return false + } + + assert.True(t, match(Service.EmailDomainAllowList, "d1")) + assert.True(t, match(Service.EmailDomainAllowList, "foo.w")) + assert.True(t, match(Service.EmailDomainAllowList, "d2")) + assert.True(t, match(Service.EmailDomainAllowList, "foo.a")) + assert.False(t, match(Service.EmailDomainAllowList, "d3")) + + assert.True(t, match(Service.EmailDomainBlockList, "d3")) + assert.True(t, match(Service.EmailDomainBlockList, "foo.b")) + assert.False(t, match(Service.EmailDomainBlockList, "d1")) +} diff --git a/services/forms/user_form.go b/services/forms/user_form.go index fa8129bf8..1e04f8531 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/web/middleware" "gitea.com/go-chi/binding" + "github.com/gobwas/glob" ) // InstallForm form for installation page @@ -105,8 +106,8 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding. // IsEmailDomainListed checks whether the domain of an email address // matches a list of domains -func IsEmailDomainListed(list []string, email string) bool { - if len(list) == 0 { +func IsEmailDomainListed(globs []glob.Glob, email string) bool { + if len(globs) == 0 { return false } @@ -117,8 +118,8 @@ func IsEmailDomainListed(list []string, email string) bool { domain := strings.ToLower(email[n+1:]) - for _, v := range list { - if strings.ToLower(v) == domain { + for _, g := range globs { + if g.Match(domain) { return true } } @@ -131,12 +132,12 @@ func IsEmailDomainListed(list []string, email string) bool { // The email is marked as allowed if it matches any of the // domains in the whitelist or if it doesn't match any of // domains in the blocklist, if any such list is not empty. -func (f RegisterForm) IsEmailDomainAllowed() bool { - if len(setting.Service.EmailDomainWhitelist) == 0 { - return !IsEmailDomainListed(setting.Service.EmailDomainBlocklist, f.Email) +func (f *RegisterForm) IsEmailDomainAllowed() bool { + if len(setting.Service.EmailDomainAllowList) == 0 { + return !IsEmailDomainListed(setting.Service.EmailDomainBlockList, f.Email) } - return IsEmailDomainListed(setting.Service.EmailDomainWhitelist, f.Email) + return IsEmailDomainListed(setting.Service.EmailDomainAllowList, f.Email) } // MustChangePasswordForm form for updating your password after account creation diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go index 225686f0f..84efa25d5 100644 --- a/services/forms/user_form_test.go +++ b/services/forms/user_form_test.go @@ -10,13 +10,17 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/modules/setting" + "github.com/gobwas/glob" "github.com/stretchr/testify/assert" ) func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { - _ = setting.Service + oldService := setting.Service + defer func() { + setting.Service = oldService + }() - setting.Service.EmailDomainWhitelist = []string{} + setting.Service.EmailDomainAllowList = nil form := RegisterForm{} @@ -24,15 +28,18 @@ func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { - _ = setting.Service + oldService := setting.Service + defer func() { + setting.Service = oldService + }() - setting.Service.EmailDomainWhitelist = []string{"gitea.io"} + setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io")} tt := []struct { email string }{ - {"securitygieqqq"}, - {"hdudhdd"}, + {"invalid-email"}, + {"gitea.io"}, } for _, v := range tt { @@ -42,10 +49,13 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { } } -func TestRegisterForm_IsDomainAllowed_WhitelistedEmail(t *testing.T) { - _ = setting.Service +func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) { + oldService := setting.Service + defer func() { + setting.Service = oldService + }() - setting.Service.EmailDomainWhitelist = []string{"gitea.io"} + setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")} tt := []struct { email string @@ -53,8 +63,11 @@ func TestRegisterForm_IsDomainAllowed_WhitelistedEmail(t *testing.T) { }{ {"security@gitea.io", true}, {"security@gITea.io", true}, - {"hdudhdd", false}, + {"invalid", false}, {"seee@example.com", false}, + + {"user@my.allow", true}, + {"user@my.allow1", false}, } for _, v := range tt { @@ -64,11 +77,14 @@ func TestRegisterForm_IsDomainAllowed_WhitelistedEmail(t *testing.T) { } } -func TestRegisterForm_IsDomainAllowed_BlocklistedEmail(t *testing.T) { - _ = setting.Service +func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) { + oldService := setting.Service + defer func() { + setting.Service = oldService + }() - setting.Service.EmailDomainWhitelist = []string{} - setting.Service.EmailDomainBlocklist = []string{"gitea.io"} + setting.Service.EmailDomainAllowList = nil + setting.Service.EmailDomainBlockList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")} tt := []struct { email string @@ -76,7 +92,10 @@ func TestRegisterForm_IsDomainAllowed_BlocklistedEmail(t *testing.T) { }{ {"security@gitea.io", false}, {"security@gitea.example", true}, - {"hdudhdd", true}, + {"invalid", true}, + + {"user@my.block", false}, + {"user@my.block1", true}, } for _, v := range tt {