1
0
Эх сурвалжийг харах

httpd: add a setting to disable login methods, deprecate the previous one

the previous enabled login methods setting is hard to extend in
a backward compatible way

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 8 сар өмнө
parent
commit
69ef36b4d9

+ 27 - 20
internal/config/config.go

@@ -99,26 +99,27 @@ var (
 		DisableWWWAuthHeader: false,
 	}
 	defaultHTTPDBinding = httpd.Binding{
-		Address:             "",
-		Port:                8080,
-		EnableWebAdmin:      true,
-		EnableWebClient:     true,
-		EnableRESTAPI:       true,
-		EnabledLoginMethods: 0,
-		EnableHTTPS:         false,
-		CertificateFile:     "",
-		CertificateKeyFile:  "",
-		MinTLSVersion:       12,
-		ClientAuthType:      0,
-		TLSCipherSuites:     nil,
-		Protocols:           nil,
-		ProxyMode:           0,
-		ProxyAllowed:        nil,
-		ClientIPProxyHeader: "",
-		ClientIPHeaderDepth: 0,
-		HideLoginURL:        0,
-		RenderOpenAPI:       true,
-		Languages:           []string{"en"},
+		Address:              "",
+		Port:                 8080,
+		EnableWebAdmin:       true,
+		EnableWebClient:      true,
+		EnableRESTAPI:        true,
+		EnabledLoginMethods:  0,
+		DisabledLoginMethods: 0,
+		EnableHTTPS:          false,
+		CertificateFile:      "",
+		CertificateKeyFile:   "",
+		MinTLSVersion:        12,
+		ClientAuthType:       0,
+		TLSCipherSuites:      nil,
+		Protocols:            nil,
+		ProxyMode:            0,
+		ProxyAllowed:         nil,
+		ClientIPProxyHeader:  "",
+		ClientIPHeaderDepth:  0,
+		HideLoginURL:         0,
+		RenderOpenAPI:        true,
+		Languages:            []string{"en"},
 		OIDC: httpd.OIDC{
 			ClientID:                   "",
 			ClientSecret:               "",
@@ -1868,6 +1869,12 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
 		isSet = true
 	}
 
+	disabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__DISABLED_LOGIN_METHODS", idx), 32)
+	if ok {
+		binding.DisabledLoginMethods = int(disabledLoginMethods)
+		isSet = true
+	}
+
 	renderOpenAPI, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__RENDER_OPENAPI", idx))
 	if ok {
 		binding.RenderOpenAPI = renderOpenAPI

+ 5 - 0
internal/config/config_test.go

@@ -1192,6 +1192,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT", "0")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API", "0")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS", "3")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS", "12")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI", "0")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES", "en,es")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ")
@@ -1263,6 +1264,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_CLIENT")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
@@ -1327,6 +1329,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.True(t, bindings[0].EnableWebClient)
 	require.True(t, bindings[0].EnableRESTAPI)
 	require.Equal(t, 0, bindings[0].EnabledLoginMethods)
+	require.Equal(t, 0, bindings[0].DisabledLoginMethods)
 	require.True(t, bindings[0].RenderOpenAPI)
 	require.Len(t, bindings[0].Languages, 1)
 	assert.Contains(t, bindings[0].Languages, "en")
@@ -1348,6 +1351,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.True(t, bindings[1].EnableWebClient)
 	require.True(t, bindings[1].EnableRESTAPI)
 	require.Equal(t, 0, bindings[1].EnabledLoginMethods)
+	require.Equal(t, 0, bindings[1].DisabledLoginMethods)
 	require.True(t, bindings[1].RenderOpenAPI)
 	require.Len(t, bindings[1].Languages, 1)
 	assert.Contains(t, bindings[1].Languages, "en")
@@ -1370,6 +1374,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.False(t, bindings[2].EnableWebClient)
 	require.False(t, bindings[2].EnableRESTAPI)
 	require.Equal(t, 3, bindings[2].EnabledLoginMethods)
+	require.Equal(t, 12, bindings[2].DisabledLoginMethods)
 	require.False(t, bindings[2].RenderOpenAPI)
 	require.Len(t, bindings[2].Languages, 2)
 	assert.Contains(t, bindings[2].Languages, "en")

+ 65 - 17
internal/httpd/httpd.go

@@ -416,7 +416,7 @@ type SecurityConf struct {
 	PermissionsPolicy string `json:"permissions_policy" mapstructure:"permissions_policy"`
 	// CrossOriginOpenerPolicy allows to set the Cross-Origin-Opener-Policy header value. Default is "".
 	CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy" mapstructure:"cross_origin_opener_policy"`
-	// CrossOriginResourcePolicy allows to set the Cross-Origin-Opener-Policy header value. Default is "".
+	// CrossOriginResourcePolicy allows to set the Cross-Origin-Resource-Policy header value. Default is "".
 	CrossOriginResourcePolicy string `json:"cross_origin_resource_policy" mapstructure:"cross_origin_resource_policy"`
 	// CrossOriginEmbedderPolicy allows to set the Cross-Origin-Embedder-Policy header value. Default is "".
 	CrossOriginEmbedderPolicy string `json:"cross_origin_embedder_policy" mapstructure:"cross_origin_embedder_policy"`
@@ -565,7 +565,21 @@ type Binding struct {
 	//
 	// You can combine the values. For example 3 means that you can only login using OIDC on
 	// both WebClient and WebAdmin UI.
+	// Deprecated because it is not extensible, use DisabledLoginMethods
 	EnabledLoginMethods int `json:"enabled_login_methods" mapstructure:"enabled_login_methods"`
+	// Defines the login methods disabled for the WebAdmin and WebClient UIs:
+	//
+	// - 1 means OIDC for the WebAdmin UI
+	// - 2 means OIDC for the WebClient UI
+	// - 4 means login form for the WebAdmin UI
+	// - 8 means login form for the WebClient UI
+	// - 16 means basic auth for admin REST API
+	// - 32 means basic auth for user REST API
+	// - 64 means API key auth for admins
+	// - 128 means API key auth for users
+	// You can combine the values. For example 12 means that you can only login using OIDC on
+	// both WebClient and WebAdmin UI.
+	DisabledLoginMethods int `json:"disabled_login_methods" mapstructure:"disabled_login_methods"`
 	// you also need to provide a certificate for enabling HTTPS
 	EnableHTTPS bool `json:"enable_https" mapstructure:"enable_https"`
 	// Certificate and matching private key for this specific binding, if empty the global
@@ -687,45 +701,76 @@ func (b *Binding) IsValid() bool {
 
 func (b *Binding) isWebAdminOIDCLoginDisabled() bool {
 	if b.EnableWebAdmin {
-		if b.EnabledLoginMethods == 0 {
-			return false
-		}
-		return b.EnabledLoginMethods&1 == 0
+		return b.DisabledLoginMethods&1 != 0
 	}
 	return false
 }
 
 func (b *Binding) isWebClientOIDCLoginDisabled() bool {
 	if b.EnableWebClient {
-		if b.EnabledLoginMethods == 0 {
-			return false
-		}
-		return b.EnabledLoginMethods&2 == 0
+		return b.DisabledLoginMethods&2 != 0
 	}
 	return false
 }
 
 func (b *Binding) isWebAdminLoginFormDisabled() bool {
 	if b.EnableWebAdmin {
-		if b.EnabledLoginMethods == 0 {
-			return false
-		}
-		return b.EnabledLoginMethods&4 == 0
+		return b.DisabledLoginMethods&4 != 0
 	}
 	return false
 }
 
 func (b *Binding) isWebClientLoginFormDisabled() bool {
 	if b.EnableWebClient {
-		if b.EnabledLoginMethods == 0 {
-			return false
-		}
-		return b.EnabledLoginMethods&8 == 0
+		return b.DisabledLoginMethods&8 != 0
 	}
 	return false
 }
 
+func (b *Binding) isAdminTokenEndpointDisabled() bool {
+	return b.DisabledLoginMethods&16 != 0
+}
+
+func (b *Binding) isUserTokenEndpointDisabled() bool {
+	return b.DisabledLoginMethods&32 != 0
+}
+
+func (b *Binding) isAdminAPIKeyAuthDisabled() bool {
+	return b.DisabledLoginMethods&64 != 0
+}
+
+func (b *Binding) isUserAPIKeyAuthDisabled() bool {
+	return b.DisabledLoginMethods&128 != 0
+}
+
+func (b *Binding) hasLoginForAPI() bool {
+	return !b.isAdminTokenEndpointDisabled() || !b.isUserTokenEndpointDisabled() ||
+		!b.isAdminAPIKeyAuthDisabled() || !b.isUserAPIKeyAuthDisabled()
+}
+
+// convertLoginMethods checks if the deprecated EnabledLoginMethods is set and
+// convert the value to DisabledLoginMethods.
+func (b *Binding) convertLoginMethods() {
+	if b.DisabledLoginMethods > 0 || b.EnabledLoginMethods == 0 {
+		// DisabledLoginMethods already in use or EnabledLoginMethods not set.
+		return
+	}
+	if b.EnabledLoginMethods&1 == 0 {
+		b.DisabledLoginMethods++
+	}
+	if b.EnabledLoginMethods&2 == 0 {
+		b.DisabledLoginMethods += 2
+	}
+	if b.EnabledLoginMethods&4 == 0 {
+		b.DisabledLoginMethods += 4
+	}
+	if b.EnabledLoginMethods&8 == 0 {
+		b.DisabledLoginMethods += 8
+	}
+}
+
 func (b *Binding) checkLoginMethods() error {
+	b.convertLoginMethods()
 	if b.isWebAdminLoginFormDisabled() && b.isWebAdminOIDCLoginDisabled() {
 		return errors.New("no login method available for WebAdmin UI")
 	}
@@ -742,6 +787,9 @@ func (b *Binding) checkLoginMethods() error {
 			return errors.New("no login method available for WebClient UI")
 		}
 	}
+	if b.EnableRESTAPI && !b.hasLoginForAPI() {
+		return errors.New("no login method available for REST API")
+	}
 	return nil
 }
 

+ 12 - 5
internal/httpd/httpd_test.go

@@ -578,27 +578,28 @@ func TestInitialization(t *testing.T) {
 	httpdConf.Bindings[0].OIDC = httpd.OIDC{}
 	httpdConf.Bindings[0].EnableWebClient = true
 	httpdConf.Bindings[0].EnableWebAdmin = true
-	httpdConf.Bindings[0].EnabledLoginMethods = 1
+	httpdConf.Bindings[0].EnableRESTAPI = true
+	httpdConf.Bindings[0].DisabledLoginMethods = 14
 	err = httpdConf.Initialize(configDir, isShared)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no login method available for WebAdmin UI")
 	}
-	httpdConf.Bindings[0].EnabledLoginMethods = 2
+	httpdConf.Bindings[0].DisabledLoginMethods = 13
 	err = httpdConf.Initialize(configDir, isShared)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no login method available for WebAdmin UI")
 	}
-	httpdConf.Bindings[0].EnabledLoginMethods = 6
+	httpdConf.Bindings[0].DisabledLoginMethods = 9
 	err = httpdConf.Initialize(configDir, isShared)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no login method available for WebClient UI")
 	}
-	httpdConf.Bindings[0].EnabledLoginMethods = 4
+	httpdConf.Bindings[0].DisabledLoginMethods = 11
 	err = httpdConf.Initialize(configDir, isShared)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no login method available for WebClient UI")
 	}
-	httpdConf.Bindings[0].EnabledLoginMethods = 3
+	httpdConf.Bindings[0].DisabledLoginMethods = 12
 	err = httpdConf.Initialize(configDir, isShared)
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no login method available for WebAdmin UI")
@@ -608,6 +609,12 @@ func TestInitialization(t *testing.T) {
 	if assert.Error(t, err) {
 		assert.Contains(t, err.Error(), "no login method available for WebClient UI")
 	}
+	httpdConf.Bindings[0].EnableWebClient = false
+	httpdConf.Bindings[0].DisabledLoginMethods = 240
+	err = httpdConf.Initialize(configDir, isShared)
+	if assert.Error(t, err) {
+		assert.Contains(t, err.Error(), "no login method available for REST API")
+	}
 	err = dataprovider.Close()
 	assert.NoError(t, err)
 	err = httpdConf.Initialize(configDir, isShared)

+ 143 - 0
internal/httpd/internal_test.go

@@ -3899,6 +3899,116 @@ func TestHTTPSRedirect(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestDisabledAdminLoginMethods(t *testing.T) {
+	server := httpdServer{
+		binding: Binding{
+			Address:              "",
+			Port:                 8080,
+			EnableWebAdmin:       true,
+			EnableWebClient:      true,
+			EnableRESTAPI:        true,
+			DisabledLoginMethods: 20,
+		},
+		enableWebAdmin:  true,
+		enableWebClient: true,
+		enableRESTAPI:   true,
+	}
+	server.initializeRouter()
+	testServer := httptest.NewServer(server.router)
+	defer testServer.Close()
+
+	rr := httptest.NewRecorder()
+	req, err := http.NewRequest(http.MethodGet, tokenPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, path.Join(adminPath, defaultAdminUsername, "forgot-password"), nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, path.Join(adminPath, defaultAdminUsername, "reset-password"), nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, webAdminLoginPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, webAdminResetPwdPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, webAdminForgotPwdPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+}
+
+func TestDisabledUserLoginMethods(t *testing.T) {
+	server := httpdServer{
+		binding: Binding{
+			Address:              "",
+			Port:                 8080,
+			EnableWebAdmin:       true,
+			EnableWebClient:      true,
+			EnableRESTAPI:        true,
+			DisabledLoginMethods: 40,
+		},
+		enableWebAdmin:  true,
+		enableWebClient: true,
+		enableRESTAPI:   true,
+	}
+	server.initializeRouter()
+	testServer := httptest.NewServer(server.router)
+	defer testServer.Close()
+
+	rr := httptest.NewRecorder()
+	req, err := http.NewRequest(http.MethodGet, userTokenPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, userPath+"/user/forgot-password", nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, userPath+"/user/reset-password", nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, webClientLoginPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, webClientResetPwdPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+
+	rr = httptest.NewRecorder()
+	req, err = http.NewRequest(http.MethodPost, webClientForgotPwdPath, nil)
+	require.NoError(t, err)
+	testServer.Config.Handler.ServeHTTP(rr, req)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+}
+
 func TestGetLogEventString(t *testing.T) {
 	assert.Equal(t, "Login failed", getLogEventString(notifier.LogEventTypeLoginFailed))
 	assert.Equal(t, "Login with non-existent user", getLogEventString(notifier.LogEventTypeLoginNoUser))
@@ -4066,6 +4176,39 @@ func TestI18NErrors(t *testing.T) {
 	assert.Equal(t, `{"a":"b"}`, errI18n.Args())
 }
 
+func TestConvertEnabledLoginMethods(t *testing.T) {
+	b := Binding{
+		EnabledLoginMethods:  0,
+		DisabledLoginMethods: 1,
+	}
+	b.convertLoginMethods()
+	assert.Equal(t, 1, b.DisabledLoginMethods)
+	b.DisabledLoginMethods = 0
+	b.EnabledLoginMethods = 1
+	b.convertLoginMethods()
+	assert.Equal(t, 14, b.DisabledLoginMethods)
+	b.DisabledLoginMethods = 0
+	b.EnabledLoginMethods = 2
+	b.convertLoginMethods()
+	assert.Equal(t, 13, b.DisabledLoginMethods)
+	b.DisabledLoginMethods = 0
+	b.EnabledLoginMethods = 3
+	b.convertLoginMethods()
+	assert.Equal(t, 12, b.DisabledLoginMethods)
+	b.DisabledLoginMethods = 0
+	b.EnabledLoginMethods = 4
+	b.convertLoginMethods()
+	assert.Equal(t, 11, b.DisabledLoginMethods)
+	b.DisabledLoginMethods = 0
+	b.EnabledLoginMethods = 7
+	b.convertLoginMethods()
+	assert.Equal(t, 8, b.DisabledLoginMethods)
+	b.DisabledLoginMethods = 0
+	b.EnabledLoginMethods = 15
+	b.convertLoginMethods()
+	assert.Equal(t, 0, b.DisabledLoginMethods)
+}
+
 func getCSRFTokenFromBody(body io.Reader) (string, error) {
 	doc, err := html.Parse(body)
 	if err != nil {

+ 1 - 1
internal/httpd/oidc_test.go

@@ -1548,7 +1548,7 @@ func TestOIDCWithLoginFormsDisabled(t *testing.T) {
 
 	server := getTestOIDCServer()
 	server.binding.OIDC.ImplicitRoles = true
-	server.binding.EnabledLoginMethods = 3
+	server.binding.DisabledLoginMethods = 12
 	server.binding.EnableWebAdmin = true
 	server.binding.EnableWebClient = true
 	err := server.binding.OIDC.initialize()

+ 60 - 48
internal/httpd/server.go

@@ -1298,23 +1298,55 @@ func (s *httpdServer) initializeRouter() {
 		}
 	}
 
-	if s.enableRESTAPI {
-		// share API available to external users
-		s.router.Get(sharesPath+"/{id}", s.downloadFromShare) //nolint:goconst
-		s.router.Post(sharesPath+"/{id}", s.uploadFilesToShare)
-		s.router.Post(sharesPath+"/{id}/{name}", s.uploadFileToShare)
-		s.router.With(compressor.Handler).Get(sharesPath+"/{id}/dirs", s.readBrowsableShareContents)
-		s.router.Get(sharesPath+"/{id}/files", s.downloadBrowsableSharedFile)
+	s.setupRESTAPIRoutes()
+
+	if s.enableWebAdmin || s.enableWebClient {
+		s.router.Group(func(router chi.Router) {
+			router.Use(cleanCacheControlMiddleware)
+			router.Use(compressor.Handler)
+			serveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)
+		})
+		if s.binding.OIDC.isEnabled() {
+			s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect)
+		}
+		if s.enableWebClient {
+			s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
+				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+				s.redirectToWebPath(w, r, webClientLoginPath)
+			})
+			s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
+				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+				s.redirectToWebPath(w, r, webClientLoginPath)
+			})
+		} else {
+			s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
+				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+				s.redirectToWebPath(w, r, webAdminLoginPath)
+			})
+			s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
+				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+				s.redirectToWebPath(w, r, webAdminLoginPath)
+			})
+		}
+	}
+
+	s.setupWebClientRoutes()
+	s.setupWebAdminRoutes()
+}
 
-		s.router.Get(tokenPath, s.getToken)
-		s.router.Post(adminPath+"/{username}/forgot-password", forgotAdminPassword)
-		s.router.Post(adminPath+"/{username}/reset-password", resetAdminPassword)
-		s.router.Post(userPath+"/{username}/forgot-password", forgotUserPassword)
-		s.router.Post(userPath+"/{username}/reset-password", resetUserPassword)
+func (s *httpdServer) setupRESTAPIRoutes() {
+	if s.enableRESTAPI {
+		if !s.binding.isAdminTokenEndpointDisabled() {
+			s.router.Get(tokenPath, s.getToken)
+			s.router.Post(adminPath+"/{username}/forgot-password", forgotAdminPassword)
+			s.router.Post(adminPath+"/{username}/reset-password", resetAdminPassword)
+		}
 
 		s.router.Group(func(router chi.Router) {
 			router.Use(checkNodeToken(s.tokenAuth))
-			router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeAdmin))
+			if !s.binding.isAdminAPIKeyAuthDisabled() {
+				router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeAdmin))
+			}
 			router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
 			router.Use(jwtAuthenticatorAPI)
 
@@ -1429,10 +1461,23 @@ func (s *httpdServer) initializeRouter() {
 			})
 		})
 
-		s.router.Get(userTokenPath, s.getUserToken)
+		// share API available to external users
+		s.router.Get(sharesPath+"/{id}", s.downloadFromShare)
+		s.router.Post(sharesPath+"/{id}", s.uploadFilesToShare)
+		s.router.Post(sharesPath+"/{id}/{name}", s.uploadFileToShare)
+		s.router.With(compressor.Handler).Get(sharesPath+"/{id}/dirs", s.readBrowsableShareContents)
+		s.router.Get(sharesPath+"/{id}/files", s.downloadBrowsableSharedFile)
+
+		if !s.binding.isUserTokenEndpointDisabled() {
+			s.router.Get(userTokenPath, s.getUserToken)
+			s.router.Post(userPath+"/{username}/forgot-password", forgotUserPassword)
+			s.router.Post(userPath+"/{username}/reset-password", resetUserPassword)
+		}
 
 		s.router.Group(func(router chi.Router) {
-			router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeUser))
+			if !s.binding.isUserAPIKeyAuthDisabled() {
+				router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeUser))
+			}
 			router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
 			router.Use(jwtAuthenticatorAPIUser)
 
@@ -1498,39 +1543,6 @@ func (s *httpdServer) initializeRouter() {
 			})
 		}
 	}
-
-	if s.enableWebAdmin || s.enableWebClient {
-		s.router.Group(func(router chi.Router) {
-			router.Use(cleanCacheControlMiddleware)
-			router.Use(compressor.Handler)
-			serveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)
-		})
-		if s.binding.OIDC.isEnabled() {
-			s.router.Get(webOIDCRedirectPath, s.handleOIDCRedirect)
-		}
-		if s.enableWebClient {
-			s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
-				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-				s.redirectToWebPath(w, r, webClientLoginPath)
-			})
-			s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
-				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-				s.redirectToWebPath(w, r, webClientLoginPath)
-			})
-		} else {
-			s.router.Get(webRootPath, func(w http.ResponseWriter, r *http.Request) {
-				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-				s.redirectToWebPath(w, r, webAdminLoginPath)
-			})
-			s.router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
-				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-				s.redirectToWebPath(w, r, webAdminLoginPath)
-			})
-		}
-	}
-
-	s.setupWebClientRoutes()
-	s.setupWebAdminRoutes()
 }
 
 func (s *httpdServer) setupWebClientRoutes() {

+ 1 - 0
sftpgo.json

@@ -272,6 +272,7 @@
         "enable_web_client": true,
         "enable_rest_api": true,
         "enabled_login_methods": 0,
+        "disabled_login_methods": 0,
         "enable_https": false,
         "certificate_file": "",
         "certificate_key_file": "",