web setup: add an optional installation code

The purpose of this code is to prevent anyone who can access to
the initial setup screen from creating an admin user

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-02-27 13:08:47 +01:00
parent 7f674a7fb3
commit dcc3292dbc
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
12 changed files with 218 additions and 31 deletions

View file

@ -39,10 +39,11 @@ const (
) )
var ( var (
globalConf globalConfig globalConf globalConfig
defaultSFTPDBanner = fmt.Sprintf("SFTPGo_%v", version.Get().Version) defaultSFTPDBanner = fmt.Sprintf("SFTPGo_%v", version.Get().Version)
defaultFTPDBanner = fmt.Sprintf("SFTPGo %v ready", version.Get().Version) defaultFTPDBanner = fmt.Sprintf("SFTPGo %v ready", version.Get().Version)
defaultSFTPDBinding = sftpd.Binding{ defaultInstallCodeHint = "Installation code"
defaultSFTPDBinding = sftpd.Binding{
Address: "", Address: "",
Port: 2022, Port: 2022,
ApplyProxyConfig: true, ApplyProxyConfig: true,
@ -323,6 +324,10 @@ func Init() {
AllowCredentials: false, AllowCredentials: false,
MaxAge: 0, MaxAge: 0,
}, },
Setup: httpd.SetupConfig{
InstallationCode: "",
InstallationCodeHint: defaultInstallCodeHint,
},
}, },
HTTPConfig: httpclient.Config{ HTTPConfig: httpclient.Config{
Timeout: 20, Timeout: 20,
@ -354,7 +359,6 @@ func Init() {
MinTLSVersion: 12, MinTLSVersion: 12,
TLSCipherSuites: nil, TLSCipherSuites: nil,
}, },
PluginsConfig: nil,
SMTPConfig: smtp.Config{ SMTPConfig: smtp.Config{
Host: "", Host: "",
Port: 25, Port: 25,
@ -366,6 +370,7 @@ func Init() {
Domain: "", Domain: "",
TemplatesPath: "templates", TemplatesPath: "templates",
}, },
PluginsConfig: nil,
} }
viper.SetEnvPrefix(configEnvPrefix) viper.SetEnvPrefix(configEnvPrefix)
@ -497,7 +502,10 @@ func HasServicesToStart() bool {
return false return false
} }
func getRedactedPassword() string { func getRedactedPassword(value string) string {
if value == "" {
return value
}
return "[redacted]" return "[redacted]"
} }
@ -509,22 +517,19 @@ func getRedactedGlobalConf() globalConfig {
conf.Common.PostDisconnectHook = util.GetRedactedURL(conf.Common.PostDisconnectHook) conf.Common.PostDisconnectHook = util.GetRedactedURL(conf.Common.PostDisconnectHook)
conf.Common.DataRetentionHook = util.GetRedactedURL(conf.Common.DataRetentionHook) conf.Common.DataRetentionHook = util.GetRedactedURL(conf.Common.DataRetentionHook)
conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook) conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)
conf.HTTPDConfig.SigningPassphrase = getRedactedPassword() conf.HTTPDConfig.SigningPassphrase = getRedactedPassword(conf.HTTPDConfig.SigningPassphrase)
conf.ProviderConf.Password = getRedactedPassword() conf.HTTPDConfig.Setup.InstallationCode = getRedactedPassword(conf.HTTPDConfig.Setup.InstallationCode)
conf.ProviderConf.Password = getRedactedPassword(conf.ProviderConf.Password)
conf.ProviderConf.Actions.Hook = util.GetRedactedURL(conf.ProviderConf.Actions.Hook) conf.ProviderConf.Actions.Hook = util.GetRedactedURL(conf.ProviderConf.Actions.Hook)
conf.ProviderConf.ExternalAuthHook = util.GetRedactedURL(conf.ProviderConf.ExternalAuthHook) conf.ProviderConf.ExternalAuthHook = util.GetRedactedURL(conf.ProviderConf.ExternalAuthHook)
conf.ProviderConf.PreLoginHook = util.GetRedactedURL(conf.ProviderConf.PreLoginHook) conf.ProviderConf.PreLoginHook = util.GetRedactedURL(conf.ProviderConf.PreLoginHook)
conf.ProviderConf.PostLoginHook = util.GetRedactedURL(conf.ProviderConf.PostLoginHook) conf.ProviderConf.PostLoginHook = util.GetRedactedURL(conf.ProviderConf.PostLoginHook)
conf.ProviderConf.CheckPasswordHook = util.GetRedactedURL(conf.ProviderConf.CheckPasswordHook) conf.ProviderConf.CheckPasswordHook = util.GetRedactedURL(conf.ProviderConf.CheckPasswordHook)
conf.SMTPConfig.Password = getRedactedPassword() conf.SMTPConfig.Password = getRedactedPassword(conf.SMTPConfig.Password)
conf.HTTPDConfig.Bindings = nil conf.HTTPDConfig.Bindings = nil
for _, binding := range globalConf.HTTPDConfig.Bindings { for _, binding := range globalConf.HTTPDConfig.Bindings {
if binding.OIDC.ClientID != "" { binding.OIDC.ClientID = getRedactedPassword(binding.OIDC.ClientID)
binding.OIDC.ClientID = getRedactedPassword() binding.OIDC.ClientSecret = getRedactedPassword(binding.OIDC.ClientSecret)
}
if binding.OIDC.ClientSecret != "" {
binding.OIDC.ClientSecret = getRedactedPassword()
}
conf.HTTPDConfig.Bindings = append(conf.HTTPDConfig.Bindings, binding) conf.HTTPDConfig.Bindings = append(conf.HTTPDConfig.Bindings, binding)
} }
return conf return conf
@ -576,6 +581,18 @@ func LoadConfig(configDir, configFile string) error {
return nil return nil
} }
func isUploadModeValid() bool {
return globalConf.Common.UploadMode >= 0 && globalConf.Common.UploadMode <= 2
}
func isProxyProtocolValid() bool {
return globalConf.Common.ProxyProtocol >= 0 && globalConf.Common.ProxyProtocol <= 2
}
func isExternalAuthScopeValid() bool {
return globalConf.ProviderConf.ExternalAuthScope >= 0 && globalConf.ProviderConf.ExternalAuthScope <= 15
}
func resetInvalidConfigs() { func resetInvalidConfigs() {
if strings.TrimSpace(globalConf.SFTPD.Banner) == "" { if strings.TrimSpace(globalConf.SFTPD.Banner) == "" {
globalConf.SFTPD.Banner = defaultSFTPDBanner globalConf.SFTPD.Banner = defaultSFTPDBanner
@ -583,27 +600,30 @@ func resetInvalidConfigs() {
if strings.TrimSpace(globalConf.FTPD.Banner) == "" { if strings.TrimSpace(globalConf.FTPD.Banner) == "" {
globalConf.FTPD.Banner = defaultFTPDBanner globalConf.FTPD.Banner = defaultFTPDBanner
} }
if strings.TrimSpace(globalConf.HTTPDConfig.Setup.InstallationCodeHint) == "" {
globalConf.HTTPDConfig.Setup.InstallationCodeHint = defaultInstallCodeHint
}
if globalConf.ProviderConf.UsersBaseDir != "" && !util.IsFileInputValid(globalConf.ProviderConf.UsersBaseDir) { if globalConf.ProviderConf.UsersBaseDir != "" && !util.IsFileInputValid(globalConf.ProviderConf.UsersBaseDir) {
warn := fmt.Sprintf("invalid users base dir %#v will be ignored", globalConf.ProviderConf.UsersBaseDir) warn := fmt.Sprintf("invalid users base dir %#v will be ignored", globalConf.ProviderConf.UsersBaseDir)
globalConf.ProviderConf.UsersBaseDir = "" globalConf.ProviderConf.UsersBaseDir = ""
logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn) logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn)
logger.WarnToConsole("Non-fatal configuration error: %v", warn) logger.WarnToConsole("Non-fatal configuration error: %v", warn)
} }
if globalConf.Common.UploadMode < 0 || globalConf.Common.UploadMode > 2 { if !isUploadModeValid() {
warn := fmt.Sprintf("invalid upload_mode 0, 1 and 2 are supported, configured: %v reset upload_mode to 0", warn := fmt.Sprintf("invalid upload_mode 0, 1 and 2 are supported, configured: %v reset upload_mode to 0",
globalConf.Common.UploadMode) globalConf.Common.UploadMode)
globalConf.Common.UploadMode = 0 globalConf.Common.UploadMode = 0
logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn) logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn)
logger.WarnToConsole("Non-fatal configuration error: %v", warn) logger.WarnToConsole("Non-fatal configuration error: %v", warn)
} }
if globalConf.Common.ProxyProtocol < 0 || globalConf.Common.ProxyProtocol > 2 { if !isProxyProtocolValid() {
warn := fmt.Sprintf("invalid proxy_protocol 0, 1 and 2 are supported, configured: %v reset proxy_protocol to 0", warn := fmt.Sprintf("invalid proxy_protocol 0, 1 and 2 are supported, configured: %v reset proxy_protocol to 0",
globalConf.Common.ProxyProtocol) globalConf.Common.ProxyProtocol)
globalConf.Common.ProxyProtocol = 0 globalConf.Common.ProxyProtocol = 0
logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn) logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn)
logger.WarnToConsole("Non-fatal configuration error: %v", warn) logger.WarnToConsole("Non-fatal configuration error: %v", warn)
} }
if globalConf.ProviderConf.ExternalAuthScope < 0 || globalConf.ProviderConf.ExternalAuthScope > 15 { if !isExternalAuthScopeValid() {
warn := fmt.Sprintf("invalid external_auth_scope: %v reset to 0", globalConf.ProviderConf.ExternalAuthScope) warn := fmt.Sprintf("invalid external_auth_scope: %v reset to 0", globalConf.ProviderConf.ExternalAuthScope)
globalConf.ProviderConf.ExternalAuthScope = 0 globalConf.ProviderConf.ExternalAuthScope = 0
logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn) logger.Warn(logSender, "", "Non-fatal configuration error: %v", warn)
@ -1555,6 +1575,8 @@ func setViperDefaults() {
viper.SetDefault("httpd.cors.allowed_headers", globalConf.HTTPDConfig.Cors.AllowedHeaders) viper.SetDefault("httpd.cors.allowed_headers", globalConf.HTTPDConfig.Cors.AllowedHeaders)
viper.SetDefault("httpd.cors.exposed_headers", globalConf.HTTPDConfig.Cors.ExposedHeaders) viper.SetDefault("httpd.cors.exposed_headers", globalConf.HTTPDConfig.Cors.ExposedHeaders)
viper.SetDefault("httpd.cors.allow_credentials", globalConf.HTTPDConfig.Cors.AllowCredentials) viper.SetDefault("httpd.cors.allow_credentials", globalConf.HTTPDConfig.Cors.AllowCredentials)
viper.SetDefault("httpd.setup.installation_code", globalConf.HTTPDConfig.Setup.InstallationCode)
viper.SetDefault("httpd.setup.installation_code_hint", globalConf.HTTPDConfig.Setup.InstallationCodeHint)
viper.SetDefault("httpd.cors.max_age", globalConf.HTTPDConfig.Cors.MaxAge) viper.SetDefault("httpd.cors.max_age", globalConf.HTTPDConfig.Cors.MaxAge)
viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout) viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout)
viper.SetDefault("http.retry_wait_min", globalConf.HTTPConfig.RetryWaitMin) viper.SetDefault("http.retry_wait_min", globalConf.HTTPConfig.RetryWaitMin)
@ -1581,7 +1603,6 @@ func setViperDefaults() {
viper.SetDefault("smtp.auth_type", globalConf.SMTPConfig.AuthType) viper.SetDefault("smtp.auth_type", globalConf.SMTPConfig.AuthType)
viper.SetDefault("smtp.encryption", globalConf.SMTPConfig.Encryption) viper.SetDefault("smtp.encryption", globalConf.SMTPConfig.Encryption)
viper.SetDefault("smtp.domain", globalConf.SMTPConfig.Domain) viper.SetDefault("smtp.domain", globalConf.SMTPConfig.Domain)
viper.SetDefault("smtp.templates_path", globalConf.SMTPConfig.TemplatesPath)
} }
func lookupBoolFromEnv(envName string) (bool, bool) { func lookupBoolFromEnv(envName string) (bool, bool) {

View file

@ -250,6 +250,34 @@ func TestInvalidUsersBaseDir(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestInvalidInstallationHint(t *testing.T) {
reset()
configDir := ".."
confName := tempConfigName + ".json"
configFilePath := filepath.Join(configDir, confName)
err := config.LoadConfig(configDir, "")
assert.NoError(t, err)
httpdConfig := config.GetHTTPDConfig()
httpdConfig.Setup = httpd.SetupConfig{
InstallationCode: "abc",
InstallationCodeHint: " ",
}
c := make(map[string]httpd.Conf)
c["httpd"] = httpdConfig
jsonConf, err := json.Marshal(c)
assert.NoError(t, err)
err = os.WriteFile(configFilePath, jsonConf, os.ModePerm)
assert.NoError(t, err)
err = config.LoadConfig(configDir, confName)
assert.NoError(t, err)
httpdConfig = config.GetHTTPDConfig()
assert.Equal(t, "abc", httpdConfig.Setup.InstallationCode)
assert.Equal(t, "Installation code", httpdConfig.Setup.InstallationCodeHint)
err = os.Remove(configFilePath)
assert.NoError(t, err)
}
func TestDefenderProviderDriver(t *testing.T) { func TestDefenderProviderDriver(t *testing.T) {
if config.GetProviderConf().Driver != dataprovider.SQLiteDataProviderName { if config.GetProviderConf().Driver != dataprovider.SQLiteDataProviderName {
t.Skip("this test is not supported with the current database provider") t.Skip("this test is not supported with the current database provider")
@ -1094,6 +1122,7 @@ func TestConfigFromEnv(t *testing.T) {
os.Setenv("SFTPGO_KMS__SECRETS__URL", "local") os.Setenv("SFTPGO_KMS__SECRETS__URL", "local")
os.Setenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH", "path") os.Setenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH", "path")
os.Setenv("SFTPGO_TELEMETRY__TLS_CIPHER_SUITES", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") os.Setenv("SFTPGO_TELEMETRY__TLS_CIPHER_SUITES", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA")
os.Setenv("SFTPGO_HTTPD__SETUP__INSTALLATION_CODE", "123")
t.Cleanup(func() { t.Cleanup(func() {
os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS") os.Unsetenv("SFTPGO_SFTPD__BINDINGS__0__ADDRESS")
os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__0__PORT") os.Unsetenv("SFTPGO_WEBDAVD__BINDINGS__0__PORT")
@ -1104,6 +1133,7 @@ func TestConfigFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_KMS__SECRETS__URL") os.Unsetenv("SFTPGO_KMS__SECRETS__URL")
os.Unsetenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH") os.Unsetenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH")
os.Unsetenv("SFTPGO_TELEMETRY__TLS_CIPHER_SUITES") os.Unsetenv("SFTPGO_TELEMETRY__TLS_CIPHER_SUITES")
os.Unsetenv("SFTPGO_HTTPD__SETUP__INSTALLATION_CODE")
}) })
err := config.LoadConfig(".", "invalid config") err := config.LoadConfig(".", "invalid config")
assert.NoError(t, err) assert.NoError(t, err)
@ -1123,4 +1153,5 @@ func TestConfigFromEnv(t *testing.T) {
assert.Len(t, telemetryConfig.TLSCipherSuites, 2) assert.Len(t, telemetryConfig.TLSCipherSuites, 2)
assert.Equal(t, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", telemetryConfig.TLSCipherSuites[0]) assert.Equal(t, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", telemetryConfig.TLSCipherSuites[0])
assert.Equal(t, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", telemetryConfig.TLSCipherSuites[1]) assert.Equal(t, "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", telemetryConfig.TLSCipherSuites[1])
assert.Equal(t, "123", config.GetHTTPDConfig().Setup.InstallationCode)
} }

View file

@ -145,7 +145,7 @@ func getMySQLConnectionString(redactedPwd bool) (string, error) {
var connectionString string var connectionString string
if config.ConnectionString == "" { if config.ConnectionString == "" {
password := config.Password password := config.Password
if redactedPwd { if redactedPwd && password != "" {
password = "[redacted]" password = "[redacted]"
} }
sslMode := getSSLMode() sslMode := getSSLMode()

View file

@ -150,7 +150,7 @@ func getPGSQLConnectionString(redactedPwd bool) string {
var connectionString string var connectionString string
if config.ConnectionString == "" { if config.ConnectionString == "" {
password := config.Password password := config.Password
if redactedPwd { if redactedPwd && password != "" {
password = "[redacted]" password = "[redacted]"
} }
connectionString = fmt.Sprintf("host='%v' port=%v dbname='%v' user='%v' password='%v' sslmode=%v connect_timeout=10", connectionString = fmt.Sprintf("host='%v' port=%v dbname='%v' user='%v' password='%v' sslmode=%v connect_timeout=10",

View file

@ -276,6 +276,9 @@ The configuration file contains the following sections:
- `exposed_headers`, list of strings. - `exposed_headers`, list of strings.
- `allow_credentials` boolean. - `allow_credentials` boolean.
- `max_age`, integer. - `max_age`, integer.
- `setup` struct containing configurations for the initial setup screen
- `installation_code`, string. If set, this installation code will be required when creating the first admin account. Please note that even if set using an environment variable this field is read at SFTPGo startup and not at runtime. This is not a license key or similar, the purpose here is to prevent anyone who can access to the initial setup screen from creating an admin user. Default: blank.
- `installation_code_hint`, string. Description for the installation code input field. Default: `Installation code`.
- **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server) - **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server)
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 0 - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 0
- `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: `127.0.0.1` - `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: `127.0.0.1`

View file

@ -228,7 +228,9 @@ var (
webStaticFilesPath string webStaticFilesPath string
webOpenAPIPath string webOpenAPIPath string
// max upload size for http clients, 1GB by default // max upload size for http clients, 1GB by default
maxUploadFileSize = int64(1048576000) maxUploadFileSize = int64(1048576000)
installationCode string
installationCodeHint string
) )
func init() { func init() {
@ -426,6 +428,18 @@ type ServicesStatus struct {
MFA mfa.ServiceStatus `json:"mfa"` MFA mfa.ServiceStatus `json:"mfa"`
} }
// SetupConfig defines the configuration parameters for the initial web admin setup
type SetupConfig struct {
// Installation code to require when creating the first admin account.
// As for the other configurations, this value is read at SFTPGo startup and not at runtime
// even if set using an environment variable.
// This is not a license key or similar, the purpose here is to prevent anyone who can access
// to the initial setup screen from creating an admin user
InstallationCode string `json:"installation_code" mapstructure:"installation_code"`
// Description for the installation code input field
InstallationCodeHint string `json:"installation_code_hint" mapstructure:"installation_code_hint"`
}
// CorsConfig defines the CORS configuration // CorsConfig defines the CORS configuration
type CorsConfig struct { type CorsConfig struct {
AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"` AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"`
@ -474,6 +488,8 @@ type Conf struct {
MaxUploadFileSize int64 `json:"max_upload_file_size" mapstructure:"max_upload_file_size"` MaxUploadFileSize int64 `json:"max_upload_file_size" mapstructure:"max_upload_file_size"`
// CORS configuration // CORS configuration
Cors CorsConfig `json:"cors" mapstructure:"cors"` Cors CorsConfig `json:"cors" mapstructure:"cors"`
// Initial setup configuration
Setup SetupConfig `json:"setup" mapstructure:"setup"`
} }
type apiResponse struct { type apiResponse struct {
@ -521,7 +537,12 @@ func (c *Conf) checkRequiredDirs(staticFilesPath, templatesPath string) error {
func (c *Conf) getRedacted() Conf { func (c *Conf) getRedacted() Conf {
redacted := "[redacted]" redacted := "[redacted]"
conf := *c conf := *c
conf.SigningPassphrase = redacted if conf.SigningPassphrase != "" {
conf.SigningPassphrase = redacted
}
if conf.Setup.InstallationCode != "" {
conf.Setup.InstallationCode = redacted
}
conf.Bindings = nil conf.Bindings = nil
for _, binding := range c.Bindings { for _, binding := range c.Bindings {
if binding.OIDC.ClientID != "" { if binding.OIDC.ClientID != "" {
@ -604,6 +625,8 @@ func (c *Conf) Initialize(configDir string) error {
} }
maxUploadFileSize = c.MaxUploadFileSize maxUploadFileSize = c.MaxUploadFileSize
installationCode = c.Setup.InstallationCode
installationCodeHint = c.Setup.InstallationCodeHint
startCleanupTicker(tokenDuration / 2) startCleanupTicker(tokenDuration / 2)
return <-exitChannel return <-exitChannel
} }

View file

@ -266,6 +266,7 @@ G6p7xS+JswJrzX4885bZJ9Oi1AR2yM3sC9l0O7I4lDbNPmWIXBLeEhGMmcPKv/Kc
w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p
xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw== xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
-----END RSA PRIVATE KEY-----` -----END RSA PRIVATE KEY-----`
defaultAdminUsername = "admin"
) )
type failingWriter struct { type failingWriter struct {
@ -300,6 +301,21 @@ func TestShouldBind(t *testing.T) {
} }
} }
func TestRedactedConf(t *testing.T) {
c := Conf{
SigningPassphrase: "passphrase",
Setup: SetupConfig{
InstallationCode: "123",
},
}
redactedField := "[redacted]"
redactedConf := c.getRedacted()
assert.Equal(t, redactedField, redactedConf.SigningPassphrase)
assert.Equal(t, redactedField, redactedConf.Setup.InstallationCode)
assert.NotEqual(t, c.SigningPassphrase, redactedConf.SigningPassphrase)
assert.NotEqual(t, c.Setup.InstallationCode, redactedConf.Setup.InstallationCode)
}
func TestGetRespStatus(t *testing.T) { func TestGetRespStatus(t *testing.T) {
var err error var err error
err = util.NewMethodDisabledError("") err = util.NewMethodDisabledError("")
@ -708,7 +724,7 @@ func TestCreateTokenError(t *testing.T) {
} }
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
admin := dataprovider.Admin{ admin := dataprovider.Admin{
Username: "admin", Username: defaultAdminUsername,
Password: "password", Password: "password",
} }
req, _ := http.NewRequest(http.MethodGet, tokenPath, nil) req, _ := http.NewRequest(http.MethodGet, tokenPath, nil)
@ -918,7 +934,7 @@ func TestAPIKeyAuthForbidden(t *testing.T) {
func TestJWTTokenValidation(t *testing.T) { func TestJWTTokenValidation(t *testing.T) {
tokenAuth := jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil) tokenAuth := jwtauth.New(jwa.HS256.String(), util.GenerateRandomBytes(32), nil)
claims := make(map[string]interface{}) claims := make(map[string]interface{})
claims["username"] = "admin" claims["username"] = defaultAdminUsername
claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour) claims[jwt.ExpirationKey] = time.Now().UTC().Add(-1 * time.Hour)
token, _, err := tokenAuth.Encode(claims) token, _, err := tokenAuth.Encode(claims)
assert.NoError(t, err) assert.NoError(t, err)
@ -2308,3 +2324,77 @@ func TestSecureMiddlewareIntegration(t *testing.T) {
server.binding.Security.updateProxyHeaders() server.binding.Security.updateProxyHeaders()
assert.Len(t, server.binding.Security.proxyHeaders, 0) assert.Len(t, server.binding.Security.proxyHeaders, 0)
} }
func TestWebAdminSetupWithInstallCode(t *testing.T) {
installationCode = "1234"
// delete all the admins
admins, err := dataprovider.GetAdmins(100, 0, dataprovider.OrderASC)
assert.NoError(t, err)
for _, admin := range admins {
err = dataprovider.DeleteAdmin(admin.Username, "", "")
assert.NoError(t, err)
}
// close the provider and initializes it without creating the default admin
providerConf := dataprovider.GetProviderConfig()
providerConf.CreateDefaultAdmin = false
err = dataprovider.Close()
assert.NoError(t, err)
err = dataprovider.Initialize(providerConf, "..", true)
assert.NoError(t, err)
server := httpdServer{
enableWebAdmin: true,
enableWebClient: true,
}
server.initializeRouter()
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, webAdminSetupPath, nil)
assert.NoError(t, err)
server.router.ServeHTTP(rr, r)
assert.Equal(t, http.StatusOK, rr.Code)
for _, webURL := range []string{"/", webBasePath, webBaseAdminPath, webAdminLoginPath, webClientLoginPath} {
rr = httptest.NewRecorder()
r, err = http.NewRequest(http.MethodGet, webURL, nil)
assert.NoError(t, err)
server.router.ServeHTTP(rr, r)
assert.Equal(t, http.StatusFound, rr.Code)
assert.Equal(t, webAdminSetupPath, rr.Header().Get("Location"))
}
form := make(url.Values)
csrfToken := createCSRFToken()
form.Set("_form_token", csrfToken)
form.Set("install_code", "12345")
form.Set("username", defaultAdminUsername)
form.Set("password", "password")
form.Set("confirm_password", "password")
rr = httptest.NewRecorder()
r, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
server.router.ServeHTTP(rr, r)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "Installation code mismatch")
_, err = dataprovider.AdminExists(defaultAdminUsername)
assert.Error(t, err)
form.Set("install_code", "1234")
rr = httptest.NewRecorder()
r, err = http.NewRequest(http.MethodPost, webAdminSetupPath, bytes.NewBuffer([]byte(form.Encode())))
assert.NoError(t, err)
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
server.router.ServeHTTP(rr, r)
assert.Equal(t, http.StatusFound, rr.Code)
_, err = dataprovider.AdminExists(defaultAdminUsername)
assert.NoError(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
providerConf.CreateDefaultAdmin = true
err = dataprovider.Initialize(providerConf, "..", true)
assert.NoError(t, err)
installationCode = ""
}

View file

@ -603,6 +603,11 @@ func (s *httpdServer) handleWebAdminSetupPost(w http.ResponseWriter, r *http.Req
} }
username := r.Form.Get("username") username := r.Form.Get("username")
password := r.Form.Get("password") password := r.Form.Get("password")
installCode := r.Form.Get("install_code")
if installationCode != "" && installCode != installationCode {
renderAdminSetupPage(w, r, username, fmt.Sprintf("%v mismatch", installationCodeHint))
return
}
confirmPassword := r.Form.Get("confirm_password") confirmPassword := r.Form.Get("confirm_password")
if username == "" { if username == "" {
renderAdminSetupPage(w, r, username, "Please set a username") renderAdminSetupPage(w, r, username, "Please set a username")

View file

@ -212,8 +212,10 @@ type defenderHostsPage struct {
type setupPage struct { type setupPage struct {
basePage basePage
Username string Username string
Error string HasInstallationCode bool
InstallationCodeHint string
Error string
} }
type folderPage struct { type folderPage struct {
@ -553,9 +555,11 @@ func renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string)
func renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) { func renderAdminSetupPage(w http.ResponseWriter, r *http.Request, username, error string) {
data := setupPage{ data := setupPage{
basePage: getBasePageData(pageSetupTitle, webAdminSetupPath, r), basePage: getBasePageData(pageSetupTitle, webAdminSetupPath, r),
Username: username, Username: username,
Error: error, HasInstallationCode: installationCode != "",
InstallationCodeHint: installationCodeHint,
Error: error,
} }
renderAdminTemplate(w, templateSetup, data) renderAdminTemplate(w, templateSetup, data)

View file

@ -247,7 +247,7 @@ func (s *Service) configurePortableUser() string {
s.PortableUser.Username = "user" s.PortableUser.Username = "user"
} }
printablePassword := "" printablePassword := ""
if len(s.PortableUser.Password) > 0 { if s.PortableUser.Password != "" {
printablePassword = "[redacted]" printablePassword = "[redacted]"
} }
if len(s.PortableUser.PublicKeys) == 0 && s.PortableUser.Password == "" { if len(s.PortableUser.PublicKeys) == 0 && s.PortableUser.Password == "" {

View file

@ -264,6 +264,10 @@
"exposed_headers": [], "exposed_headers": [],
"allow_credentials": false, "allow_credentials": false,
"max_age": 0 "max_age": 0
},
"setup": {
"installation_code": "",
"installation_code_hint": "Installation code"
} }
}, },
"telemetry": { "telemetry": {

View file

@ -97,6 +97,12 @@
{{end}} {{end}}
<form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off" <form id="login_form" action="{{.CurrentURL}}" method="POST" autocomplete="off"
class="user-custom"> class="user-custom">
{{if .HasInstallationCode}}
<div class="form-group">
<input type="text" class="form-control form-control-user-custom" id="inputInstallCode"
name="install_code" placeholder="{{.InstallationCodeHint}}" value="" required>
</div>
{{end}}
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control form-control-user-custom" id="inputUsername" <input type="text" class="form-control form-control-user-custom" id="inputUsername"
name="username" placeholder="Username" value="{{.Username}}" required> name="username" placeholder="Username" value="{{.Username}}" required>