From 4be6307d87c2e10de64ea64fe0e132f4c7c5fe85 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Tue, 8 Jun 2021 13:24:28 +0200 Subject: [PATCH] webadmin: add defender page --- dataprovider/admin.go | 2 +- dataprovider/dataprovider.go | 15 +- docs/defender.md | 9 +- httpd/api_defender.go | 7 +- httpd/httpd.go | 6 + httpd/httpd_test.go | 21 +++ httpd/server.go | 3 + httpd/webadmin.go | 57 ++++---- templates/webadmin/admins.html | 8 +- templates/webadmin/base.html | 8 ++ templates/webadmin/connections.html | 8 +- templates/webadmin/defender.html | 213 ++++++++++++++++++++++++++++ templates/webadmin/folders.html | 8 +- templates/webadmin/users.html | 8 +- templates/webclient/files.html | 6 +- 15 files changed, 322 insertions(+), 57 deletions(-) create mode 100644 templates/webadmin/defender.html diff --git a/dataprovider/admin.go b/dataprovider/admin.go index d2da8fbf..b116e59c 100644 --- a/dataprovider/admin.go +++ b/dataprovider/admin.go @@ -66,7 +66,7 @@ type Admin struct { } func (a *Admin) checkPassword() error { - if a.Password != "" && !strings.HasPrefix(a.Password, argonPwdPrefix) { + if a.Password != "" && !utils.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) { if config.PasswordHashing.Algo == HashingAlgoBcrypt { pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost) if err != nil { diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index c6325146..0c792db6 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -118,13 +118,14 @@ var ( // ErrNoInitRequired defines the error returned by InitProvider if no inizialization/update is required ErrNoInitRequired = errors.New("the data provider is up to date") // ErrInvalidCredentials defines the error to return if the supplied credentials are invalid - ErrInvalidCredentials = errors.New("invalid credentials") - isAdminCreated = int32(0) - validTLSUsernames = []string{string(TLSUsernameNone), string(TLSUsernameCN)} - config Config - provider Provider - sqlPlaceholders []string - hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, + ErrInvalidCredentials = errors.New("invalid credentials") + isAdminCreated = int32(0) + validTLSUsernames = []string{string(TLSUsernameNone), string(TLSUsernameCN)} + config Config + provider Provider + sqlPlaceholders []string + internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix} + hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix} pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix} pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix} diff --git a/docs/defender.md b/docs/defender.md index a646eeab..d703fbd5 100644 --- a/docs/defender.md +++ b/docs/defender.md @@ -26,13 +26,10 @@ The `ban_time_increment` is calculated as percentage of `ban_time`, so if `ban_t The `defender` will keep in memory both the host scores and the banned hosts, you can limit the memory usage using the `entries_soft_limit` and `entries_hard_limit` configuration keys. -The REST API allows: +Using the REST API you can: -- to retrieve the score for an IP address -- to retrieve the ban time for an IP address -- to unban an IP address - -We don't return the whole list of the banned IP addresses or all stored scores because we store them as a hash map and iterating over all the keys of a hash map is not a fast operation and will slow down the recordings of new events. +- list hosts within the defender's lists +- remove hosts from the defender's lists The `defender` can also load a permanent block list and/or a safe list of ip addresses/networks from a file: diff --git a/httpd/api_defender.go b/httpd/api_defender.go index 7a32db0c..c558387f 100644 --- a/httpd/api_defender.go +++ b/httpd/api_defender.go @@ -14,7 +14,12 @@ import ( ) func getDefenderHosts(w http.ResponseWriter, r *http.Request) { - render.JSON(w, r, common.GetDefenderHosts()) + hosts := common.GetDefenderHosts() + if hosts == nil { + render.JSON(w, r, make([]common.DefenderEntry, 0)) + return + } + render.JSON(w, r, hosts) } func getDefenderHostByID(w http.ResponseWriter, r *http.Request) { diff --git a/httpd/httpd.go b/httpd/httpd.go index fdc74d41..faa6381e 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -82,6 +82,8 @@ const ( webChangeAdminPwdPathDefault = "/web/admin/changepwd" webTemplateUserDefault = "/web/admin/template/user" webTemplateFolderDefault = "/web/admin/template/folder" + webDefenderPathDefault = "/web/admin/defender" + webDefenderHostsPathDefault = "/web/admin/defender/hosts" webClientLoginPathDefault = "/web/client/login" webClientFilesPathDefault = "/web/client/files" webClientDirContentsPathDefault = "/web/client/listdir" @@ -128,6 +130,8 @@ var ( webChangeAdminPwdPath string webTemplateUser string webTemplateFolder string + webDefenderPath string + webDefenderHostsPath string webClientLoginPath string webClientFilesPath string webClientDirContentsPath string @@ -460,6 +464,8 @@ func updateWebAdminURLs(baseURL string) { webChangeAdminPwdPath = path.Join(baseURL, webChangeAdminPwdPathDefault) webTemplateUser = path.Join(baseURL, webTemplateUserDefault) webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault) + webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault) + webDefenderPath = path.Join(baseURL, webDefenderPathDefault) webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault) } diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 4f566b0f..e2e8918b 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -99,6 +99,7 @@ const ( webChangeAdminPwdPath = "/web/admin/changepwd" webTemplateUser = "/web/admin/template/user" webTemplateFolder = "/web/admin/template/folder" + webDefenderPath = "/web/admin/defender" webBasePathClient = "/web/client" webClientLoginPath = "/web/client/login" webClientFilesPath = "/web/client/files" @@ -3842,6 +3843,8 @@ func TestChangeAdminPwdMock(t *testing.T) { func TestUpdateAdminMock(t *testing.T) { token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) assert.NoError(t, err) + _, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass) + assert.Error(t, err) admin := getTestAdmin() admin.Username = altAdminUsername admin.Permissions = []string{dataprovider.PermAdminManageAdmins} @@ -3851,6 +3854,9 @@ func TestUpdateAdminMock(t *testing.T) { setBearerForReq(req, token) rr := executeRequest(req) checkResponseCode(t, http.StatusCreated, rr) + _, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodPut, path.Join(adminPath, "abc"), bytes.NewBuffer(asJSON)) setBearerForReq(req, token) rr = executeRequest(req) @@ -3885,6 +3891,7 @@ func TestUpdateAdminMock(t *testing.T) { altToken, err := getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass) assert.NoError(t, err) + admin.Password = "" // it must remain unchanged admin.Permissions = []string{dataprovider.PermAdminManageAdmins, dataprovider.PermAdminCloseConnections} asJSON, err = json.Marshal(admin) assert.NoError(t, err) @@ -3893,6 +3900,9 @@ func TestUpdateAdminMock(t *testing.T) { rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) + _, err = getJWTAPITokenFromTestServer(altAdminUsername, defaultTokenAuthPass) + assert.NoError(t, err) + req, _ = http.NewRequest(http.MethodDelete, path.Join(adminPath, altAdminUsername), nil) setBearerForReq(req, token) rr = executeRequest(req) @@ -6241,6 +6251,17 @@ func TestBasicWebUsersMock(t *testing.T) { checkResponseCode(t, http.StatusOK, rr) } +func TestRenderDefenderPageMock(t *testing.T) { + token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) + assert.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, webDefenderPath, nil) + assert.NoError(t, err) + setJWTCookieForReq(req, token) + rr := executeRequest(req) + checkResponseCode(t, http.StatusOK, rr) + assert.Contains(t, rr.Body.String(), "View and manage blocklist") +} + func TestWebAdminBasicMock(t *testing.T) { token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) assert.NoError(t, err) diff --git a/httpd/server.go b/httpd/server.go index 04790991..d24fdda7 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -742,6 +742,9 @@ func (s *httpdServer) initializeRouter() { router.With(checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie). Get(webTemplateFolder, handleWebTemplateFolderGet) router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, handleWebTemplateFolderPost) + router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, handleWebDefenderPage) + router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts) + router.With(checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}", deleteDefenderHostByID) }) } } diff --git a/httpd/webadmin.go b/httpd/webadmin.go index 278e26a3..55924ca8 100644 --- a/httpd/webadmin.go +++ b/httpd/webadmin.go @@ -52,6 +52,7 @@ const ( templateMessage = "message.html" templateStatus = "status.html" templateLogin = "login.html" + templateDefender = "defender.html" templateChangePwd = "changepwd.html" templateMaintenance = "maintenance.html" templateSetup = "adminsetup.html" @@ -62,6 +63,7 @@ const ( pageFoldersTitle = "Folders" pageChangePwdTitle = "Change password" pageMaintenanceTitle = "Maintenance" + pageDefenderTitle = "Defender" pageSetupTitle = "Create first admin user" defaultQueryLimit = 500 ) @@ -83,6 +85,7 @@ type basePage struct { FoldersURL string FolderURL string FolderTemplateURL string + DefenderURL string LogoutURL string ChangeAdminPwdURL string FolderQuotaScanURL string @@ -95,8 +98,10 @@ type basePage struct { FoldersTitle string StatusTitle string MaintenanceTitle string + DefenderTitle string Version string CSRFToken string + HasDefender bool LoggedAdmin *dataprovider.Admin } @@ -159,6 +164,11 @@ type maintenancePage struct { Error string } +type defenderHostsPage struct { + basePage + DefenderHostsURL string +} + type setupPage struct { basePage Username string @@ -234,6 +244,10 @@ func loadAdminTemplates(templatesPath string) { filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateMaintenance), } + defenderPath := []string{ + filepath.Join(templatesPath, templateAdminDir, templateBase), + filepath.Join(templatesPath, templateAdminDir, templateDefender), + } setupPath := []string{ filepath.Join(templatesPath, templateAdminDir, templateSetup), } @@ -249,6 +263,7 @@ func loadAdminTemplates(templatesPath string) { loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...)) changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...)) maintenanceTmpl := utils.LoadTemplate(template.ParseFiles(maintenancePath...)) + defenderTmpl := utils.LoadTemplate(template.ParseFiles(defenderPath...)) setupTmpl := utils.LoadTemplate(template.ParseFiles(setupPath...)) adminTemplates[templateUsers] = usersTmpl @@ -263,6 +278,7 @@ func loadAdminTemplates(templatesPath string) { adminTemplates[templateLogin] = loginTmpl adminTemplates[templateChangePwd] = changePwdTmpl adminTemplates[templateMaintenance] = maintenanceTmpl + adminTemplates[templateDefender] = defenderTmpl adminTemplates[templateSetup] = setupTmpl } @@ -282,6 +298,7 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage { FoldersURL: webFoldersPath, FolderURL: webFolderPath, FolderTemplateURL: webTemplateFolder, + DefenderURL: webDefenderPath, LogoutURL: webLogoutPath, ChangeAdminPwdURL: webChangeAdminPwdPath, QuotaScanURL: webQuotaScanPath, @@ -296,8 +313,10 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage { FoldersTitle: pageFoldersTitle, StatusTitle: pageStatusTitle, MaintenanceTitle: pageMaintenanceTitle, + DefenderTitle: pageDefenderTitle, Version: version.GetAsString(), LoggedAdmin: getAdminFromToken(r), + HasDefender: common.Config.DefenderConfig.Enabled, CSRFToken: csrfToken, } } @@ -540,35 +559,6 @@ func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder { } } - /*formValue := r.Form.Get("virtual_folders") - for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") { - if strings.Contains(cleaned, "::") { - mapping := strings.Split(cleaned, "::") - if len(mapping) > 1 { - vfolder := vfs.VirtualFolder{ - BaseVirtualFolder: vfs.BaseVirtualFolder{ - Name: strings.TrimSpace(mapping[1]), - }, - VirtualPath: strings.TrimSpace(mapping[0]), - QuotaFiles: -1, - QuotaSize: -1, - } - if len(mapping) > 2 { - quotaFiles, err := strconv.Atoi(strings.TrimSpace(mapping[2])) - if err == nil { - vfolder.QuotaFiles = quotaFiles - } - } - if len(mapping) > 3 { - quotaSize, err := strconv.ParseInt(strings.TrimSpace(mapping[3]), 10, 64) - if err == nil { - vfolder.QuotaSize = quotaSize - } - } - virtualFolders = append(virtualFolders, vfolder) - } - } - }*/ return virtualFolders } @@ -1232,6 +1222,15 @@ func handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, webAdminsPath, http.StatusSeeOther) } +func handleWebDefenderPage(w http.ResponseWriter, r *http.Request) { + data := defenderHostsPage{ + basePage: getBasePageData(pageDefenderTitle, webDefenderPath, r), + DefenderHostsURL: webDefenderHostsPath, + } + + renderAdminTemplate(w, templateDefender, data) +} + func handleGetWebUsers(w http.ResponseWriter, r *http.Request) { limit := defaultQueryLimit if _, ok := r.URL.Query()["qlimit"]; ok { diff --git a/templates/webadmin/admins.html b/templates/webadmin/admins.html index f5adc940..039ce154 100644 --- a/templates/webadmin/admins.html +++ b/templates/webadmin/admins.html @@ -107,16 +107,18 @@ headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'}, timeout: 15000, success: function (result) { - table.button('delete:name').enable(true); window.location.href = '{{.AdminsURL}}'; }, error: function ($xhr, textStatus, errorThrown) { - table.button('delete:name').enable(true); var txt = "Unable to delete the selected admin"; if ($xhr) { var json = $xhr.responseJSON; if (json) { - txt += ": " + json.error; + if (json.message){ + txt += ": " + json.message; + } else { + txt += ": " + json.error; + } } } $('#errorTxt').text(txt); diff --git a/templates/webadmin/base.html b/templates/webadmin/base.html index d12edc83..5bd51df5 100644 --- a/templates/webadmin/base.html +++ b/templates/webadmin/base.html @@ -97,6 +97,14 @@ {{end}} + {{ if and .HasDefender (.LoggedAdmin.HasPermission "view_defender")}} + + {{end}} + {{ if .LoggedAdmin.HasPermission "manage_admins"}}