mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
295 lines
13 KiB
Go
295 lines
13 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package setting
|
|
|
|
import (
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/glob"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/structs"
|
|
)
|
|
|
|
// enumerates all the types of captchas
|
|
const (
|
|
ImageCaptcha = "image"
|
|
ReCaptcha = "recaptcha"
|
|
HCaptcha = "hcaptcha"
|
|
MCaptcha = "mcaptcha"
|
|
CfTurnstile = "cfturnstile"
|
|
)
|
|
|
|
// Service settings
|
|
var Service = struct {
|
|
DefaultUserVisibility string
|
|
DefaultUserVisibilityMode structs.VisibleType
|
|
AllowedUserVisibilityModes []string
|
|
AllowedUserVisibilityModesSlice AllowedVisibility `ini:"-"`
|
|
DefaultOrgVisibility string
|
|
DefaultOrgVisibilityMode structs.VisibleType
|
|
ActiveCodeLives int
|
|
ResetPwdCodeLives int
|
|
RegisterEmailConfirm bool
|
|
RegisterManualConfirm bool
|
|
EmailDomainAllowList []glob.Glob
|
|
EmailDomainBlockList []glob.Glob
|
|
DisableRegistration bool
|
|
AllowOnlyInternalRegistration bool
|
|
AllowOnlyExternalRegistration bool
|
|
ShowRegistrationButton bool
|
|
EnablePasswordSignInForm bool
|
|
ShowMilestonesDashboardPage bool
|
|
RequireSignInViewStrict bool
|
|
BlockAnonymousAccessExpensive bool
|
|
EnableNotifyMail bool
|
|
EnableBasicAuth bool
|
|
EnablePasskeyAuth bool
|
|
EnableReverseProxyAuth bool
|
|
EnableReverseProxyAuthAPI bool
|
|
EnableReverseProxyAutoRegister bool
|
|
EnableReverseProxyEmail bool
|
|
EnableReverseProxyFullName bool
|
|
EnableCaptcha bool
|
|
RequireCaptchaForLogin bool
|
|
RequireExternalRegistrationCaptcha bool
|
|
RequireExternalRegistrationPassword bool
|
|
CaptchaType string
|
|
RecaptchaSecret string
|
|
RecaptchaSitekey string
|
|
RecaptchaURL string
|
|
CfTurnstileSecret string
|
|
CfTurnstileSitekey string
|
|
HcaptchaSecret string
|
|
HcaptchaSitekey string
|
|
McaptchaSecret string
|
|
McaptchaSitekey string
|
|
McaptchaURL string
|
|
DefaultKeepEmailPrivate bool
|
|
DefaultAllowCreateOrganization bool
|
|
DefaultUserIsRestricted bool
|
|
EnableTimetracking bool
|
|
DefaultEnableTimetracking bool
|
|
DefaultEnableDependencies bool
|
|
AllowCrossRepositoryDependencies bool
|
|
DefaultAllowOnlyContributorsToTrackTime bool
|
|
NoReplyAddress string
|
|
UserLocationMapURL string
|
|
EnableUserHeatmap bool
|
|
AutoWatchNewRepos bool
|
|
AutoWatchOnChanges bool
|
|
DefaultOrgMemberVisible bool
|
|
UserDeleteWithCommentsMaxTime time.Duration
|
|
ValidSiteURLSchemes []string
|
|
|
|
// OpenID settings
|
|
EnableOpenIDSignIn bool
|
|
EnableOpenIDSignUp bool
|
|
OpenIDWhitelist []*regexp.Regexp
|
|
OpenIDBlacklist []*regexp.Regexp
|
|
|
|
// Explore page settings
|
|
Explore struct {
|
|
RequireSigninView bool `ini:"REQUIRE_SIGNIN_VIEW"`
|
|
DisableUsersPage bool `ini:"DISABLE_USERS_PAGE"`
|
|
DisableOrganizationsPage bool `ini:"DISABLE_ORGANIZATIONS_PAGE"`
|
|
DisableCodePage bool `ini:"DISABLE_CODE_PAGE"`
|
|
} `ini:"service.explore"`
|
|
|
|
QoS struct {
|
|
Enabled bool
|
|
MaxInFlightRequests int
|
|
MaxWaitingRequests int
|
|
TargetWaitTime time.Duration
|
|
}
|
|
}{
|
|
AllowedUserVisibilityModesSlice: []bool{true, true, true},
|
|
}
|
|
|
|
// AllowedVisibility store in a 3 item bool array what is allowed
|
|
type AllowedVisibility []bool
|
|
|
|
// IsAllowedVisibility check if a AllowedVisibility allow a specific VisibleType
|
|
func (a AllowedVisibility) IsAllowedVisibility(t structs.VisibleType) bool {
|
|
if int(t) >= len(a) {
|
|
return false
|
|
}
|
|
return a[t]
|
|
}
|
|
|
|
// ToVisibleTypeSlice convert a AllowedVisibility into a VisibleType slice
|
|
func (a AllowedVisibility) ToVisibleTypeSlice() (result []structs.VisibleType) {
|
|
for i, v := range a {
|
|
if v {
|
|
result = append(result, structs.VisibleType(i))
|
|
}
|
|
}
|
|
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)
|
|
Service.ResetPwdCodeLives = sec.Key("RESET_PASSWD_CODE_LIVE_MINUTES").MustInt(180)
|
|
Service.DisableRegistration = sec.Key("DISABLE_REGISTRATION").MustBool()
|
|
Service.AllowOnlyInternalRegistration = sec.Key("ALLOW_ONLY_INTERNAL_REGISTRATION").MustBool()
|
|
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
|
|
if Service.AllowOnlyExternalRegistration && Service.AllowOnlyInternalRegistration {
|
|
log.Warn("ALLOW_ONLY_INTERNAL_REGISTRATION and ALLOW_ONLY_EXTERNAL_REGISTRATION are true - disabling registration")
|
|
Service.DisableRegistration = true
|
|
}
|
|
if !sec.Key("REGISTER_EMAIL_CONFIRM").MustBool() {
|
|
Service.RegisterManualConfirm = sec.Key("REGISTER_MANUAL_CONFIRM").MustBool(false)
|
|
} else {
|
|
Service.RegisterManualConfirm = false
|
|
}
|
|
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)
|
|
|
|
// boolean values are considered as "strict"
|
|
var err error
|
|
Service.RequireSignInViewStrict, err = sec.Key("REQUIRE_SIGNIN_VIEW").Bool()
|
|
if s := sec.Key("REQUIRE_SIGNIN_VIEW").String(); err != nil && s != "" {
|
|
// non-boolean value only supports "expensive" at the moment
|
|
Service.BlockAnonymousAccessExpensive = s == "expensive"
|
|
if !Service.BlockAnonymousAccessExpensive {
|
|
log.Fatal("Invalid config option: REQUIRE_SIGNIN_VIEW = %s", s)
|
|
}
|
|
}
|
|
|
|
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
|
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
|
|
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
|
|
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
|
|
Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
|
|
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
|
|
Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool()
|
|
Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool()
|
|
Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool(false)
|
|
Service.RequireCaptchaForLogin = sec.Key("REQUIRE_CAPTCHA_FOR_LOGIN").MustBool(false)
|
|
Service.RequireExternalRegistrationCaptcha = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA").MustBool(Service.EnableCaptcha)
|
|
Service.RequireExternalRegistrationPassword = sec.Key("REQUIRE_EXTERNAL_REGISTRATION_PASSWORD").MustBool()
|
|
Service.CaptchaType = sec.Key("CAPTCHA_TYPE").MustString(ImageCaptcha)
|
|
Service.RecaptchaSecret = sec.Key("RECAPTCHA_SECRET").MustString("")
|
|
Service.RecaptchaSitekey = sec.Key("RECAPTCHA_SITEKEY").MustString("")
|
|
Service.RecaptchaURL = sec.Key("RECAPTCHA_URL").MustString("https://www.google.com/recaptcha/")
|
|
Service.CfTurnstileSecret = sec.Key("CF_TURNSTILE_SECRET").MustString("")
|
|
Service.CfTurnstileSitekey = sec.Key("CF_TURNSTILE_SITEKEY").MustString("")
|
|
Service.HcaptchaSecret = sec.Key("HCAPTCHA_SECRET").MustString("")
|
|
Service.HcaptchaSitekey = sec.Key("HCAPTCHA_SITEKEY").MustString("")
|
|
Service.McaptchaURL = sec.Key("MCAPTCHA_URL").MustString("https://demo.mcaptcha.org/")
|
|
Service.McaptchaSecret = sec.Key("MCAPTCHA_SECRET").MustString("")
|
|
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
|
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
|
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
|
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
|
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
|
if Service.EnableTimetracking {
|
|
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
|
}
|
|
Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true)
|
|
Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true)
|
|
Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true)
|
|
Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply." + Domain)
|
|
Service.UserLocationMapURL = sec.Key("USER_LOCATION_MAP_URL").String()
|
|
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
|
|
Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true)
|
|
Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false)
|
|
modes := sec.Key("ALLOWED_USER_VISIBILITY_MODES").Strings(",")
|
|
if len(modes) != 0 {
|
|
Service.AllowedUserVisibilityModes = []string{}
|
|
Service.AllowedUserVisibilityModesSlice = []bool{false, false, false}
|
|
for _, sMode := range modes {
|
|
if tp, ok := structs.VisibilityModes[sMode]; ok { // remove unsupported modes
|
|
Service.AllowedUserVisibilityModes = append(Service.AllowedUserVisibilityModes, sMode)
|
|
Service.AllowedUserVisibilityModesSlice[tp] = true
|
|
} else {
|
|
log.Warn("ALLOWED_USER_VISIBILITY_MODES %s is unsupported", sMode)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(Service.AllowedUserVisibilityModes) == 0 {
|
|
Service.AllowedUserVisibilityModes = []string{"public", "limited", "private"}
|
|
Service.AllowedUserVisibilityModesSlice = []bool{true, true, true}
|
|
}
|
|
|
|
Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").String()
|
|
if Service.DefaultUserVisibility == "" {
|
|
Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0]
|
|
} else if !Service.AllowedUserVisibilityModesSlice[structs.VisibilityModes[Service.DefaultUserVisibility]] {
|
|
log.Warn("DEFAULT_USER_VISIBILITY %s is wrong or not in ALLOWED_USER_VISIBILITY_MODES, using first allowed", Service.DefaultUserVisibility)
|
|
Service.DefaultUserVisibility = Service.AllowedUserVisibilityModes[0]
|
|
}
|
|
Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility]
|
|
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
|
|
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
|
|
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
|
|
Service.UserDeleteWithCommentsMaxTime = sec.Key("USER_DELETE_WITH_COMMENTS_MAX_TIME").MustDuration(0)
|
|
sec.Key("VALID_SITE_URL_SCHEMES").MustString("http,https")
|
|
Service.ValidSiteURLSchemes = sec.Key("VALID_SITE_URL_SCHEMES").Strings(",")
|
|
schemes := make([]string, 0, len(Service.ValidSiteURLSchemes))
|
|
for _, scheme := range Service.ValidSiteURLSchemes {
|
|
scheme = strings.ToLower(strings.TrimSpace(scheme))
|
|
if scheme != "" {
|
|
schemes = append(schemes, scheme)
|
|
}
|
|
}
|
|
Service.ValidSiteURLSchemes = schemes
|
|
|
|
mustMapSetting(rootCfg, "service.explore", &Service.Explore)
|
|
|
|
loadOpenIDSetting(rootCfg)
|
|
loadQosSetting(rootCfg)
|
|
}
|
|
|
|
func loadOpenIDSetting(rootCfg ConfigProvider) {
|
|
sec := rootCfg.Section("openid")
|
|
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock)
|
|
Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && Service.EnableOpenIDSignIn)
|
|
pats := sec.Key("WHITELISTED_URIS").Strings(" ")
|
|
if len(pats) != 0 {
|
|
Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats))
|
|
for i, p := range pats {
|
|
Service.OpenIDWhitelist[i] = regexp.MustCompilePOSIX(p)
|
|
}
|
|
}
|
|
pats = sec.Key("BLACKLISTED_URIS").Strings(" ")
|
|
if len(pats) != 0 {
|
|
Service.OpenIDBlacklist = make([]*regexp.Regexp, len(pats))
|
|
for i, p := range pats {
|
|
Service.OpenIDBlacklist[i] = regexp.MustCompilePOSIX(p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadQosSetting(rootCfg ConfigProvider) {
|
|
sec := rootCfg.Section("qos")
|
|
Service.QoS.Enabled = sec.Key("ENABLED").MustBool(false)
|
|
Service.QoS.MaxInFlightRequests = sec.Key("MAX_INFLIGHT").MustInt(4 * runtime.NumCPU())
|
|
Service.QoS.MaxWaitingRequests = sec.Key("MAX_WAITING").MustInt(100)
|
|
Service.QoS.TargetWaitTime = sec.Key("TARGET_WAIT_TIME").MustDuration(250 * time.Millisecond)
|
|
}
|