Explorar o código

allow to disable REST API

Fixes #987

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino %!s(int64=3) %!d(string=hai) anos
pai
achega
7ae9303c99

+ 1 - 0
docs/full-configuration.md

@@ -260,6 +260,7 @@ The configuration file contains the following sections:
     - `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: blank.
     - `enable_web_admin`, boolean. Set to `false` to disable the built-in web admin for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web admin interface. Default `true`.
     - `enable_web_client`, boolean. Set to `false` to disable the built-in web client for this binding. You also need to define `templates_path` and `static_files_path` to use the built-in web client interface. Default `true`.
+    - `enable_rest_api`, boolean. Set to `false` to disable REST API. Default `true`.
     - `enabled_login_methods`, integer. Defines the login methods available for the WebAdmin and WebClient UIs. `0` means any configured method: username/password login form and OIDC, if enabled. `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. You can combine the values. For example `3` means that you can only login using OIDC on both WebClient and WebAdmin UI. Default: `0`.
     - `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
     - `certificate_file`, string. Binding specific TLS certificate. This can be an absolute path or a path relative to the config dir.

+ 5 - 0
docs/rest-api.md

@@ -20,6 +20,8 @@ If you define multiple bindings, each binding will sign JWT tokens with a differ
 
 If, instead, you want to use a persistent signing key for JWT tokens, you can define a signing passphrase via configuration file or environment variable.
 
+REST API can be disabled within the `httpd` configuration via the `enable_rest_api` key.
+
 You can create other administrator and assign them the following permissions:
 
 - add users
@@ -35,8 +37,11 @@ You can create other administrator and assign them the following permissions:
 - manage API keys
 - manage system
 - manage admins
+- manage groups
 - manage data retention
+- manage metadata
 - view events
+- manage event rules
 
 You can also restrict administrator access based on the source IP address. If you are running SFTPGo behind a reverse proxy you need to allow both the proxy IP address and the real client IP.
 

+ 7 - 0
internal/config/config.go

@@ -100,6 +100,7 @@ var (
 		Port:                  8080,
 		EnableWebAdmin:        true,
 		EnableWebClient:       true,
+		EnableRESTAPI:         true,
 		EnabledLoginMethods:   0,
 		EnableHTTPS:           false,
 		CertificateFile:       "",
@@ -1685,6 +1686,12 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
 		isSet = true
 	}
 
+	enableRESTAPI, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_REST_API", idx))
+	if ok {
+		binding.EnableRESTAPI = enableRESTAPI
+		isSet = true
+	}
+
 	enabledLoginMethods, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLED_LOGIN_METHODS", idx))
 	if ok {
 		binding.EnabledLoginMethods = int(enabledLoginMethods)

+ 5 - 0
internal/config/config_test.go

@@ -1019,6 +1019,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PORT", "9000")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0")
 	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__RENDER_OPENAPI", "0")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ")
@@ -1088,6 +1089,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN")
 		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__RENDER_OPENAPI")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
@@ -1149,6 +1151,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 12, bindings[0].MinTLSVersion)
 	require.True(t, bindings[0].EnableWebAdmin)
 	require.True(t, bindings[0].EnableWebClient)
+	require.True(t, bindings[0].EnableRESTAPI)
 	require.Equal(t, 0, bindings[0].EnabledLoginMethods)
 	require.True(t, bindings[0].RenderOpenAPI)
 	require.Len(t, bindings[0].TLSCipherSuites, 1)
@@ -1165,6 +1168,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 12, bindings[0].MinTLSVersion)
 	require.True(t, bindings[1].EnableWebAdmin)
 	require.True(t, bindings[1].EnableWebClient)
+	require.True(t, bindings[1].EnableRESTAPI)
 	require.Equal(t, 0, bindings[1].EnabledLoginMethods)
 	require.True(t, bindings[1].RenderOpenAPI)
 	require.Nil(t, bindings[1].TLSCipherSuites)
@@ -1182,6 +1186,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, 13, bindings[2].MinTLSVersion)
 	require.False(t, bindings[2].EnableWebAdmin)
 	require.False(t, bindings[2].EnableWebClient)
+	require.False(t, bindings[2].EnableRESTAPI)
 	require.Equal(t, 3, bindings[2].EnabledLoginMethods)
 	require.False(t, bindings[2].RenderOpenAPI)
 	require.Equal(t, 1, bindings[2].ClientAuthType)

+ 5 - 0
internal/httpd/httpd.go

@@ -410,6 +410,8 @@ type Binding struct {
 	// Enable the built-in client interface.
 	// You have to define TemplatesPath and StaticFilesPath for this to work
 	EnableWebClient bool `json:"enable_web_client" mapstructure:"enable_web_client"`
+	// Enable REST API
+	EnableRESTAPI bool `json:"enable_rest_api" mapstructure:"enable_rest_api"`
 	// Defines the login methods available for the WebAdmin and WebClient UIs:
 	//
 	// - 0 means any configured method: username/password login form and OIDC, if enabled
@@ -522,6 +524,9 @@ func (b *Binding) GetAddress() string {
 
 // IsValid returns true if the binding is valid
 func (b *Binding) IsValid() bool {
+	if !b.EnableRESTAPI && !b.EnableWebAdmin && !b.EnableWebClient {
+		return false
+	}
 	if b.Port > 0 {
 		return true
 	}

+ 34 - 0
internal/httpd/internal_test.go

@@ -309,6 +309,8 @@ func TestShouldBind(t *testing.T) {
 			},
 		},
 	}
+	require.False(t, c.ShouldBind())
+	c.Bindings[0].EnableRESTAPI = true
 	require.True(t, c.ShouldBind())
 
 	c.Bindings[0].Port = 0
@@ -833,6 +835,7 @@ func TestCSRFToken(t *testing.T) {
 		Port:            8080,
 		EnableWebAdmin:  true,
 		EnableWebClient: true,
+		EnableRESTAPI:   true,
 		RenderOpenAPI:   true,
 	})
 	fn := verifyCSRFHeader(r)
@@ -1080,6 +1083,7 @@ func TestAPIKeyAuthForbidden(t *testing.T) {
 		Port:            8080,
 		EnableWebAdmin:  true,
 		EnableWebClient: true,
+		EnableRESTAPI:   true,
 		RenderOpenAPI:   true,
 	})
 	fn := forbidAPIKeyAuthentication(r)
@@ -1104,6 +1108,7 @@ func TestJWTTokenValidation(t *testing.T) {
 			Port:            8080,
 			EnableWebAdmin:  true,
 			EnableWebClient: true,
+			EnableRESTAPI:   true,
 			RenderOpenAPI:   true,
 		},
 	}
@@ -1648,6 +1653,7 @@ func TestProxyHeaders(t *testing.T) {
 		Port:                8080,
 		EnableWebAdmin:      true,
 		EnableWebClient:     false,
+		EnableRESTAPI:       true,
 		ProxyAllowed:        []string{testIP, "10.8.0.0/30"},
 		ClientIPProxyHeader: "x-forwarded-for",
 	}
@@ -1739,6 +1745,7 @@ func TestRecoverer(t *testing.T) {
 		Port:            8080,
 		EnableWebAdmin:  true,
 		EnableWebClient: false,
+		EnableRESTAPI:   true,
 	}
 	server := newHttpdServer(b, "../static", "", CorsConfig{}, "../openapi")
 	server.initializeRouter()
@@ -1859,6 +1866,7 @@ func TestWebAdminRedirect(t *testing.T) {
 		Port:            8080,
 		EnableWebAdmin:  true,
 		EnableWebClient: false,
+		EnableRESTAPI:   true,
 	}
 	server := newHttpdServer(b, "../static", "", CorsConfig{}, "../openapi")
 	server.initializeRouter()
@@ -2323,16 +2331,19 @@ func TestLoginLinks(t *testing.T) {
 	b := Binding{
 		EnableWebAdmin:  true,
 		EnableWebClient: false,
+		EnableRESTAPI:   true,
 	}
 	assert.False(t, b.showClientLoginURL())
 	b = Binding{
 		EnableWebAdmin:  false,
 		EnableWebClient: true,
+		EnableRESTAPI:   true,
 	}
 	assert.False(t, b.showAdminLoginURL())
 	b = Binding{
 		EnableWebAdmin:  true,
 		EnableWebClient: true,
+		EnableRESTAPI:   true,
 	}
 	assert.True(t, b.showAdminLoginURL())
 	assert.True(t, b.showClientLoginURL())
@@ -2489,6 +2500,7 @@ func TestSecureMiddlewareIntegration(t *testing.T) {
 		},
 		enableWebAdmin:  true,
 		enableWebClient: true,
+		enableRESTAPI:   true,
 	}
 	server.binding.Security.updateProxyHeaders()
 	err := server.binding.parseAllowedProxy()
@@ -2560,6 +2572,27 @@ func TestGetCompressedFileName(t *testing.T) {
 	require.Equal(t, fmt.Sprintf("%s-file1.zip", username), res)
 }
 
+func TestRESTAPIDisabled(t *testing.T) {
+	server := httpdServer{
+		enableWebAdmin:  true,
+		enableWebClient: true,
+		enableRESTAPI:   false,
+	}
+	server.initializeRouter()
+	assert.False(t, server.enableRESTAPI)
+	rr := httptest.NewRecorder()
+	r, err := http.NewRequest(http.MethodGet, healthzPath, nil)
+	assert.NoError(t, err)
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusOK, rr.Code)
+
+	rr = httptest.NewRecorder()
+	r, err = http.NewRequest(http.MethodGet, tokenPath, nil)
+	assert.NoError(t, err)
+	server.router.ServeHTTP(rr, r)
+	assert.Equal(t, http.StatusNotFound, rr.Code)
+}
+
 func TestWebAdminSetupWithInstallCode(t *testing.T) {
 	installationCode = "1234"
 	// delete all the admins
@@ -2580,6 +2613,7 @@ func TestWebAdminSetupWithInstallCode(t *testing.T) {
 	server := httpdServer{
 		enableWebAdmin:  true,
 		enableWebClient: true,
+		enableRESTAPI:   true,
 	}
 	server.initializeRouter()
 

+ 176 - 172
internal/httpd/server.go

@@ -57,6 +57,7 @@ type httpdServer struct {
 	openAPIPath       string
 	enableWebAdmin    bool
 	enableWebClient   bool
+	enableRESTAPI     bool
 	renderOpenAPI     bool
 	isShared          int
 	router            *chi.Mux
@@ -77,6 +78,7 @@ func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors C
 		openAPIPath:       openAPIPath,
 		enableWebAdmin:    b.EnableWebAdmin,
 		enableWebClient:   b.EnableWebClient,
+		enableRESTAPI:     b.EnableRESTAPI,
 		renderOpenAPI:     b.RenderOpenAPI,
 		signingPassphrase: signingPassphrase,
 		cors:              cors,
@@ -1178,187 +1180,189 @@ func (s *httpdServer) initializeRouter() {
 		render.PlainText(w, r, "User-agent: *\nDisallow: /")
 	})
 
-	// share API exposed 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)
-
-	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)
-
-	s.router.Group(func(router chi.Router) {
-		router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeAdmin))
-		router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
-		router.Use(jwtAuthenticatorAPI)
-
-		router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
-			r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-			render.JSON(w, r, version.Get())
-		})
+	if s.enableRESTAPI {
+		// share API exposed 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)
+
+		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)
 
-		router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
-		router.With(forbidAPIKeyAuthentication).Get(adminProfilePath, getAdminProfile)
-		router.With(forbidAPIKeyAuthentication).Put(adminProfilePath, updateAdminProfile)
-		router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
-		// admin TOTP APIs
-		router.With(forbidAPIKeyAuthentication).Get(adminTOTPConfigsPath, getTOTPConfigs)
-		router.With(forbidAPIKeyAuthentication).Post(adminTOTPGeneratePath, generateTOTPSecret)
-		router.With(forbidAPIKeyAuthentication).Post(adminTOTPValidatePath, validateTOTPPasscode)
-		router.With(forbidAPIKeyAuthentication).Post(adminTOTPSavePath, saveTOTPConfig)
-		router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)
-		router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)
-
-		router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)).
-			Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
-				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-				render.JSON(w, r, getServicesStatus())
-			})
+		s.router.Group(func(router chi.Router) {
+			router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeAdmin))
+			router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
+			router.Use(jwtAuthenticatorAPI)
 
-		router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).
-			Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
+			router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
 				r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-				render.JSON(w, r, common.Connections.GetStats())
+				render.JSON(w, r, version.Get())
 			})
 
-		router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)).
-			Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
-		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
-		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
-		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
-		router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
-		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
-		router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
-		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername)
-		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
-		router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
-		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
-		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders)
-		router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath+"/{name}", getFolderByName)
-		router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
-		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(folderPath+"/{name}", updateFolder)
-		router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath+"/{name}", deleteFolder)
-		router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)
-		router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName)
-		router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)
-		router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup)
-		router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup)
-		router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
-		router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
-		router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
-		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage",
-			updateUserQuotaUsage)
-		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage",
-			updateUserTransferQuotaUsage)
-		router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage",
-			updateFolderQuotaUsage)
-		router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
-		router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
-		router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
-		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
-		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
-		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
-		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
-		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
-		router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
-		router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
-		router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
-			startRetentionCheck)
-		router.With(s.checkPerm(dataprovider.PermAdminMetadataChecks)).Get(metadataChecksPath, getMetadataChecks)
-		router.With(s.checkPerm(dataprovider.PermAdminMetadataChecks)).Post(metadataBasePath+"/{username}/check",
-			startMetadataCheck)
-		router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
-			Get(fsEventsPath, searchFsEvents)
-		router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
-			Get(providerEventsPath, searchProviderEvents)
-		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
-			Get(apiKeysPath, getAPIKeys)
-		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
-			Post(apiKeysPath, addAPIKey)
-		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
-			Get(apiKeysPath+"/{id}", getAPIKeyByID)
-		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
-			Put(apiKeysPath+"/{id}", updateAPIKey)
-		router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
-			Delete(apiKeysPath+"/{id}", deleteAPIKey)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
-		router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
-	})
+			router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
+			router.With(forbidAPIKeyAuthentication).Get(adminProfilePath, getAdminProfile)
+			router.With(forbidAPIKeyAuthentication).Put(adminProfilePath, updateAdminProfile)
+			router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
+			// admin TOTP APIs
+			router.With(forbidAPIKeyAuthentication).Get(adminTOTPConfigsPath, getTOTPConfigs)
+			router.With(forbidAPIKeyAuthentication).Post(adminTOTPGeneratePath, generateTOTPSecret)
+			router.With(forbidAPIKeyAuthentication).Post(adminTOTPValidatePath, validateTOTPPasscode)
+			router.With(forbidAPIKeyAuthentication).Post(adminTOTPSavePath, saveTOTPConfig)
+			router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)
+			router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)
+
+			router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)).
+				Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
+					r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+					render.JSON(w, r, getServicesStatus())
+				})
+
+			router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).
+				Get(activeConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
+					r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+					render.JSON(w, r, common.Connections.GetStats())
+				})
+
+			router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)).
+				Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
+			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
+			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
+			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
+			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
+			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
+			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
+			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
+			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
+			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath, getFolders)
+			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(folderPath+"/{name}", getFolderByName)
+			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(folderPath, addFolder)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(folderPath+"/{name}", updateFolder)
+			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath+"/{name}", deleteFolder)
+			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)
+			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName)
+			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)
+			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup)
+			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
+			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage",
+				updateUserQuotaUsage)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage",
+				updateUserTransferQuotaUsage)
+			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage",
+				updateFolderQuotaUsage)
+			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
+			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
+			router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
+			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
+			router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
+			router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
+				startRetentionCheck)
+			router.With(s.checkPerm(dataprovider.PermAdminMetadataChecks)).Get(metadataChecksPath, getMetadataChecks)
+			router.With(s.checkPerm(dataprovider.PermAdminMetadataChecks)).Post(metadataBasePath+"/{username}/check",
+				startMetadataCheck)
+			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+				Get(fsEventsPath, searchFsEvents)
+			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+				Get(providerEventsPath, searchProviderEvents)
+			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
+				Get(apiKeysPath, getAPIKeys)
+			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
+				Post(apiKeysPath, addAPIKey)
+			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
+				Get(apiKeysPath+"/{id}", getAPIKeyByID)
+			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
+				Put(apiKeysPath+"/{id}", updateAPIKey)
+			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
+				Delete(apiKeysPath+"/{id}", deleteAPIKey)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
+			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
+		})
 
-	s.router.Get(userTokenPath, s.getUserToken)
-
-	s.router.Group(func(router chi.Router) {
-		router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeUser))
-		router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
-		router.Use(jwtAuthenticatorAPIUser)
-
-		router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)
-		router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement,
-			s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).Put(userPwdPath, changeUserPassword)
-		router.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)
-		router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement).Put(userProfilePath, updateUserProfile)
-		// user TOTP APIs
-		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
-			Get(userTOTPConfigsPath, getTOTPConfigs)
-		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
-			Post(userTOTPGeneratePath, generateTOTPSecret)
-		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
-			Post(userTOTPValidatePath, validateTOTPPasscode)
-		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
-			Post(userTOTPSavePath, saveTOTPConfig)
-		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
-			Get(user2FARecoveryCodesPath, getRecoveryCodes)
-		router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
-			Post(user2FARecoveryCodesPath, generateRecoveryCodes)
-
-		router.With(s.checkSecondFactorRequirement, compressor.Handler).Get(userDirsPath, readUserFolder)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Post(userDirsPath, createUserDir)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Patch(userDirsPath, renameUserDir)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Delete(userDirsPath, deleteUserDir)
-		router.With(s.checkSecondFactorRequirement).Get(userFilesPath, getUserFile)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Post(userFilesPath, uploadUserFiles)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Patch(userFilesPath, renameUserFile)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Delete(userFilesPath, deleteUserFile)
-		router.With(s.checkSecondFactorRequirement).Post(userStreamZipPath, getUserFilesAsZipStream)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-			Get(userSharesPath, getShares)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-			Post(userSharesPath, addShare)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-			Get(userSharesPath+"/{id}", getShareByID)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-			Put(userSharesPath+"/{id}", updateShare)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
-			Delete(userSharesPath+"/{id}", deleteShare)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Post(userUploadFilePath, uploadUserFile)
-		router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
-			Patch(userFilesDirsMetadataPath, setFileDirMetadata)
-	})
+		s.router.Get(userTokenPath, s.getUserToken)
 
-	if s.renderOpenAPI {
 		s.router.Group(func(router chi.Router) {
-			router.Use(compressor.Handler)
-			serveStaticDir(router, webOpenAPIPath, s.openAPIPath)
+			router.Use(checkAPIKeyAuth(s.tokenAuth, dataprovider.APIKeyScopeUser))
+			router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromHeader))
+			router.Use(jwtAuthenticatorAPIUser)
+
+			router.With(forbidAPIKeyAuthentication).Get(userLogoutPath, s.logout)
+			router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement,
+				s.checkHTTPUserPerm(sdk.WebClientPasswordChangeDisabled)).Put(userPwdPath, changeUserPassword)
+			router.With(forbidAPIKeyAuthentication).Get(userProfilePath, getUserProfile)
+			router.With(forbidAPIKeyAuthentication, s.checkSecondFactorRequirement).Put(userProfilePath, updateUserProfile)
+			// user TOTP APIs
+			router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+				Get(userTOTPConfigsPath, getTOTPConfigs)
+			router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+				Post(userTOTPGeneratePath, generateTOTPSecret)
+			router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+				Post(userTOTPValidatePath, validateTOTPPasscode)
+			router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+				Post(userTOTPSavePath, saveTOTPConfig)
+			router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+				Get(user2FARecoveryCodesPath, getRecoveryCodes)
+			router.With(forbidAPIKeyAuthentication, s.checkHTTPUserPerm(sdk.WebClientMFADisabled)).
+				Post(user2FARecoveryCodesPath, generateRecoveryCodes)
+
+			router.With(s.checkSecondFactorRequirement, compressor.Handler).Get(userDirsPath, readUserFolder)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Post(userDirsPath, createUserDir)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Patch(userDirsPath, renameUserDir)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Delete(userDirsPath, deleteUserDir)
+			router.With(s.checkSecondFactorRequirement).Get(userFilesPath, getUserFile)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Post(userFilesPath, uploadUserFiles)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Patch(userFilesPath, renameUserFile)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Delete(userFilesPath, deleteUserFile)
+			router.With(s.checkSecondFactorRequirement).Post(userStreamZipPath, getUserFilesAsZipStream)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Get(userSharesPath, getShares)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Post(userSharesPath, addShare)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Get(userSharesPath+"/{id}", getShareByID)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Put(userSharesPath+"/{id}", updateShare)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
+				Delete(userSharesPath+"/{id}", deleteShare)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Post(userUploadFilePath, uploadUserFile)
+			router.With(s.checkSecondFactorRequirement, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
+				Patch(userFilesDirsMetadataPath, setFileDirMetadata)
 		})
+
+		if s.renderOpenAPI {
+			s.router.Group(func(router chi.Router) {
+				router.Use(compressor.Handler)
+				serveStaticDir(router, webOpenAPIPath, s.openAPIPath)
+			})
+		}
 	}
 
 	if s.enableWebAdmin || s.enableWebClient {

+ 1 - 0
sftpgo.json

@@ -245,6 +245,7 @@
         "address": "",
         "enable_web_admin": true,
         "enable_web_client": true,
+        "enable_rest_api": true,
         "enabled_login_methods": 0,
         "enable_https": false,
         "certificate_file": "",