web UI/REST API: add password reset

In order to reset the password from the admin/client user interface,
an SMTP configuration must be added and the user/admin must have an email
address.
You can prohibit the reset functionality on a per-user basis by using a
specific restriction.

Fixes #597
This commit is contained in:
Nicola Murino
2021-11-13 13:25:43 +01:00
parent b331dc5686
commit 78233ff9a3
25 changed files with 1787 additions and 60 deletions

View File

@@ -19,6 +19,7 @@ import (
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/mfa"
"github.com/drakkan/sftpgo/v2/sdk"
"github.com/drakkan/sftpgo/v2/smtp"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/version"
"github.com/drakkan/sftpgo/v2/vfs"
@@ -70,6 +71,8 @@ const (
pageChangePwdTitle = "Change password"
pageMaintenanceTitle = "Maintenance"
pageDefenderTitle = "Defender"
pageForgotPwdTitle = "SFTPGo Admin - Forgot password"
pageResetPwdTitle = "SFTPGo Admin - Reset password"
pageSetupTitle = "Create first admin user"
defaultQueryLimit = 500
)
@@ -250,51 +253,57 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateConnections),
}
messagePath := []string{
messagePaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateMessage),
}
foldersPath := []string{
foldersPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFolders),
}
folderPath := []string{
folderPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFsConfig),
filepath.Join(templatesPath, templateAdminDir, templateFolder),
}
statusPath := []string{
statusPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateStatus),
}
loginPath := []string{
loginPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateLogin),
}
maintenancePath := []string{
maintenancePaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateMaintenance),
}
defenderPath := []string{
defenderPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateDefender),
}
mfaPath := []string{
mfaPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateMFA),
}
twoFactorPath := []string{
twoFactorPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateTwoFactor),
}
twoFactorRecoveryPath := []string{
twoFactorRecoveryPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateTwoFactorRecovery),
}
setupPath := []string{
setupPaths := []string{
filepath.Join(templatesPath, templateAdminDir, templateBaseLogin),
filepath.Join(templatesPath, templateAdminDir, templateSetup),
}
forgotPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateForgotPassword),
}
resetPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
}
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
"ListFSProviders": sdk.ListProviders,
@@ -304,19 +313,21 @@ func loadAdminTemplates(templatesPath string) {
adminsTmpl := util.LoadTemplate(nil, adminsPaths...)
adminTmpl := util.LoadTemplate(nil, adminPaths...)
connectionsTmpl := util.LoadTemplate(nil, connectionsPaths...)
messageTmpl := util.LoadTemplate(nil, messagePath...)
foldersTmpl := util.LoadTemplate(nil, foldersPath...)
folderTmpl := util.LoadTemplate(fsBaseTpl, folderPath...)
statusTmpl := util.LoadTemplate(nil, statusPath...)
loginTmpl := util.LoadTemplate(nil, loginPath...)
messageTmpl := util.LoadTemplate(nil, messagePaths...)
foldersTmpl := util.LoadTemplate(nil, foldersPaths...)
folderTmpl := util.LoadTemplate(fsBaseTpl, folderPaths...)
statusTmpl := util.LoadTemplate(nil, statusPaths...)
loginTmpl := util.LoadTemplate(nil, loginPaths...)
profileTmpl := util.LoadTemplate(nil, profilePaths...)
changePwdTmpl := util.LoadTemplate(nil, changePwdPaths...)
maintenanceTmpl := util.LoadTemplate(nil, maintenancePath...)
defenderTmpl := util.LoadTemplate(nil, defenderPath...)
mfaTmpl := util.LoadTemplate(nil, mfaPath...)
twoFactorTmpl := util.LoadTemplate(nil, twoFactorPath...)
twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPath...)
setupTmpl := util.LoadTemplate(nil, setupPath...)
maintenanceTmpl := util.LoadTemplate(nil, maintenancePaths...)
defenderTmpl := util.LoadTemplate(nil, defenderPaths...)
mfaTmpl := util.LoadTemplate(nil, mfaPaths...)
twoFactorTmpl := util.LoadTemplate(nil, twoFactorPaths...)
twoFactorRecoveryTmpl := util.LoadTemplate(nil, twoFactorRecoveryPaths...)
setupTmpl := util.LoadTemplate(nil, setupPaths...)
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
adminTemplates[templateUsers] = usersTmpl
adminTemplates[templateUser] = userTmpl
@@ -336,6 +347,8 @@ func loadAdminTemplates(templatesPath string) {
adminTemplates[templateTwoFactor] = twoFactorTmpl
adminTemplates[templateTwoFactorRecovery] = twoFactorRecoveryTmpl
adminTemplates[templateSetup] = setupTmpl
adminTemplates[templateForgotPassword] = forgotPwdTmpl
adminTemplates[templateResetPassword] = resetPwdTmpl
}
func getBasePageData(title, currentURL string, r *http.Request) basePage {
@@ -419,6 +432,28 @@ func renderNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
renderMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
}
func renderForgotPwdPage(w http.ResponseWriter, error string) {
data := forgotPwdPage{
CurrentURL: webAdminForgotPwdPath,
Error: error,
CSRFToken: createCSRFToken(),
StaticURL: webStaticFilesPath,
Title: pageForgotPwdTitle,
}
renderAdminTemplate(w, templateForgotPassword, data)
}
func renderResetPwdPage(w http.ResponseWriter, error string) {
data := resetPwdPage{
CurrentURL: webAdminResetPwdPath,
Error: error,
CSRFToken: createCSRFToken(),
StaticURL: webStaticFilesPath,
Title: pageResetPwdTitle,
}
renderAdminTemplate(w, templateResetPassword, data)
}
func renderTwoFactorPage(w http.ResponseWriter, error string) {
data := twoFactorPage{
CurrentURL: webAdminTwoFactorPath,
@@ -1135,6 +1170,48 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
return user, err
}
func handleWebAdminForgotPwd(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
if !smtp.IsEnabled() {
renderNotFoundPage(w, r, errors.New("this page does not exist"))
return
}
renderForgotPwdPage(w, "")
}
func handleWebAdminForgotPwdPost(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
err := r.ParseForm()
if err != nil {
renderForgotPwdPage(w, err.Error())
return
}
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
renderForbiddenPage(w, r, err.Error())
return
}
username := r.Form.Get("username")
err = handleForgotPassword(r, username, true)
if err != nil {
if e, ok := err.(*util.ValidationError); ok {
renderForgotPwdPage(w, e.GetErrorString())
return
}
renderForgotPwdPage(w, err.Error())
return
}
http.Redirect(w, r, webAdminResetPwdPath, http.StatusFound)
}
func handleWebAdminPasswordReset(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
if !smtp.IsEnabled() {
renderNotFoundPage(w, r, errors.New("this page does not exist"))
return
}
renderResetPwdPage(w, "")
}
func handleWebAdminTwoFactor(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
renderTwoFactorPage(w, "")