Mercurial > gemma
view pkg/controllers/user.go @ 5637:7f9600ab5461
merge
author | Thomas Junk <thomas.junk@intevation.de> |
---|---|
date | Wed, 14 Jun 2023 08:45:11 +0200 |
parents | 79155213c4da |
children | 6270951dda28 |
line wrap: on
line source
// This is Free Software under GNU Affero General Public License v >= 3.0 // without warranty, see README.md and license for details. // // SPDX-License-Identifier: AGPL-3.0-or-later // License-Filename: LICENSES/AGPL-3.0.txt // // Copyright (C) 2018 by via donau // – Österreichische Wasserstraßen-Gesellschaft mbH // Software engineering by Intevation GmbH // // Author(s): // * Sascha L. Teichmann <sascha.teichmann@intevation.de> // * Tom Gottfried <tom.gottfried@intevation.de> // * Sascha Wilde <sascha.wilde@intevation.de> package controllers import ( "bytes" "database/sql" "fmt" "net/http" "strconv" "strings" "text/template" "time" "github.com/gorilla/mux" "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/log" "gemma.intevation.de/gemma/pkg/misc" "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/pgxutils" "gemma.intevation.de/gemma/pkg/scheduler" mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( createUserSQL = `INSERT INTO users.list_users VALUES ($1, $2, $3, $4, NULL, $5, $6, true)` createUserExtentSQL = `INSERT INTO users.list_users VALUES ($1, $2, $3, $4, ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9, $10, true)` updateUserUnprivSQL = `UPDATE users.list_users SET (pw, map_extent, email_address) = ($2, ST_MakeBox2D(ST_Point($3, $4), ST_Point($5, $6)), $7) WHERE username = $1` updateUserSQL = `UPDATE users.list_users SET (rolname, username, pw, country, map_extent, email_address, report_reciever) = ($2, $3, $4, $5, NULL, $6, $7) WHERE username = $1` updateUserExtentSQL = `UPDATE users.list_users SET (rolname, username, pw, country, map_extent, email_address, report_reciever) = ($2, $3, $4, $5, ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10, $11) WHERE username = $1` deleteUserSQL = ` WITH del AS ( DELETE FROM users.list_users WHERE username = $1 AND ( rolname NOT IN ('waterway_admin', 'sys_admin') OR NOT EXISTS (SELECT 1 FROM import.imports WHERE username = $1)) RETURNING * ), up AS ( UPDATE users.list_users SET (email_address, report_reciever, active) = ('nomail@example.com', false, false) WHERE username = $1 AND NOT EXISTS (SELECT * FROM del) RETURNING * ) SELECT EXISTS (SELECT * FROM del) AS deleted, EXISTS (SELECT * FROM up) AS updated` listUsersSQL = `SELECT rolname, username, country, email_address, ST_XMin(map_extent), ST_YMin(map_extent), ST_XMax(map_extent), ST_YMax(map_extent), report_reciever, active FROM users.list_users` listUserSQL = `SELECT rolname, country, email_address, ST_XMin(map_extent), ST_YMin(map_extent), ST_XMax(map_extent), ST_YMax(map_extent), report_reciever FROM users.list_users WHERE username = $1` ) var ( testSysadminNotifyMailTmpl = template.Must( template.New("sysadmin").Parse(`Dear {{ .User }}, this is a test email from the Gemma System Errors notification service. You recieved this mail, because a System Administrator triggered the test mail sending function at {{ .Timestamp }}. When a critical system error is detected an automated mail will be sent to {{ .Email }} with details on the error condition.`)) testWWAdminNotifyMailTmpl = template.Must( template.New("waterwayadmin").Parse(`Dear {{ .User }}, this is a test email from the Gemma System Mail notification service. You recieved this mail, because a System Administrator triggered the test mail sending function at {{ .Timestamp }}. When the status of a data import managed by you changes an automated mail will be sent to {{ .Email }} with details on the new import status (including import errors) and details on the corresponding import.`)) testWWUserNotifyMailTmpl = template.Must( template.New("waterwayuser").Parse(`Dear {{ .User }}, this is a test email from the Gemma System Mail notification service. You recieved this mail, because a System Administrator triggered the test mail sending function at {{ .Timestamp }}.`)) ) func deleteUser(req *http.Request) (jr mw.JSONResult, err error) { user := mux.Vars(req)["user"] if !models.UserName(user).IsValid() { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "error: user invalid", } return } session, _ := auth.GetSession(req) if session.User == user { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "error: cannot delete yourself", } return } ctx := req.Context() db := mw.JSONConn(req) // Remove scheduled tasks. ids, err2 := scheduler.ScheduledUserIDs(ctx, db, user) if err2 == nil { if len(ids) > 0 { go func() { scheduler.UnbindByIDs(ids) }() } } else { log.Errorf("%v\n", err2) } var deleted, updated bool if err = db.QueryRowContext(ctx, deleteUserSQL, user).Scan(&deleted, &updated); err != nil { return } var action string switch { case !deleted && !updated: err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } return case deleted: action = "deleted" case updated: action = "deactivated" default: log.Errorf("Should not happen: user '%s' updated and deleted.\n", user) action = "deleted" } // Running in a go routine should not be necessary. go func() { auth.Sessions.Logout(user) }() log.Infof("User '%s' %s.\n", user, action) jr = mw.JSONResult{ Result: struct { Action string `json:"action"` }{ Action: action, }, } return } func updateUser(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "error: user invalid", } return } newUser := mw.JSONInput(req).(*models.User) var res sql.Result db := mw.JSONConn(req) if s, _ := auth.GetSession(req); s.Roles.Has("sys_admin") { if newUser.Extent == nil { res, err = db.ExecContext( req.Context(), updateUserSQL, user, newUser.Role, newUser.User, newUser.Password, newUser.Country, newUser.Email, newUser.Reports, ) } else { res, err = db.ExecContext( req.Context(), updateUserExtentSQL, user, newUser.Role, newUser.User, newUser.Password, newUser.Country, newUser.Extent.X1, newUser.Extent.Y1, newUser.Extent.X2, newUser.Extent.Y2, newUser.Email, newUser.Reports, ) } } else { if newUser.Extent == nil { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "extent is mandatory", } return } res, err = db.ExecContext( req.Context(), updateUserUnprivSQL, user, newUser.Password, newUser.Extent.X1, newUser.Extent.Y1, newUser.Extent.X2, newUser.Extent.Y2, newUser.Email, ) } if err != nil { return } if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } return } if user != newUser.User { // Running in a go routine should not be necessary. go func() { auth.Sessions.Logout(string(user)) }() } log.Infof("User '%s' updated.\n", user) jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"` }{"success"}, } return } func patchUser(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "error: user invalid", } return } s, ok := auth.GetSession(req) if !ok { err = mw.JSONError{ Code: http.StatusUnauthorized, Message: "error: not logged in", } return } priv := s.Roles.Has("sys_admin") if !priv && s.User != string(user) { err = mw.JSONError{ Code: http.StatusUnauthorized, Message: "error: not allowed to modify someone else", } return } var ( columns []string positions []string args []interface{} ) update := func(column string, value interface{}) { columns = append(columns, column) positions = append(positions, "$"+strconv.Itoa(len(positions)+1)) args = append(args, value) } updateBox := func(column string, extent *models.BoundingBox) { columns = append(columns, column) pos := len(positions) position := fmt.Sprintf("ST_MakeBox2D(ST_Point($%d, $%d), ST_Point($%d, $%d))", pos+1, pos+2, pos+3, pos+4) positions = append(positions, position) args = append(args, extent.X1, extent.Y1, extent.X2, extent.Y2) } patch := mw.JSONInput(req).(*models.UserPatch) if patch.User != nil && priv { update("user", *patch.User) } if patch.Role != nil && priv { update("rolname", *patch.Role) } if patch.Password != nil { update("pw", *patch.Password) } if patch.Email != nil { update("email_address", *patch.Email) } if patch.Country != nil && priv { update("country", *patch.Country) } if patch.Reports != nil && priv { update("report_reciever", *patch.Reports) } if patch.Active != nil && priv { update("active", *patch.Active) } if patch.Extent != nil { updateBox("map_extent", patch.Extent) } var colsS, posS string switch len(columns) { case 0: // Nothing to do jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"` }{"success"}, } return case 1: // No brackets if there is only one argument. colsS = columns[0] posS = positions[0] default: colsS = "(" + strings.Join(columns, ",") + ")" posS = "(" + strings.Join(positions, ",") + ")" } stmt := fmt.Sprintf( `UPDATE users.list_users SET %s = %s WHERE username = $%d`, colsS, posS, len(positions)+1) args = append(args, user) db := mw.JSONConn(req) var res sql.Result if res, err = db.ExecContext(req.Context(), stmt, args...); err != nil { return } if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } return } if patch.User != nil && *patch.User != user { // Running in a go routine should not be necessary. go func() { auth.Sessions.Logout(string(user)) }() } // Log if the user should be reactivated, even if he is already active. var reactivated string if patch.Active != nil && *patch.Active && priv { reactivated = " (reactivation requested)" } log.Infof("User '%s' modified%s.\n", user, reactivated) jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"` }{"success"}, } return } func createUser(req *http.Request) (jr mw.JSONResult, err error) { user := mw.JSONInput(req).(*models.User) db := mw.JSONConn(req) if user.Extent == nil { _, err = db.ExecContext( req.Context(), createUserSQL, user.Role, user.User, user.Password, user.Country, user.Email, user.Reports, ) } else { _, err = db.ExecContext( req.Context(), createUserExtentSQL, user.Role, user.User, user.Password, user.Country, user.Extent.X1, user.Extent.Y1, user.Extent.X2, user.Extent.Y2, user.Email, user.Reports, ) } if err != nil { m, c := pgxutils.ReadableError{Err: err}.MessageAndCode() err = mw.JSONError{Code: c, Message: m} return } log.Infof("User '%s' created.\n", user.User) jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"` }{"success"}, } return } func listUsers(req *http.Request) (jr mw.JSONResult, err error) { var rows *sql.Rows rows, err = mw.JSONConn(req).QueryContext(req.Context(), listUsersSQL) if err != nil { return } defer rows.Close() var users []*models.User for rows.Next() { user := &models.User{Extent: &models.BoundingBox{}} if err = rows.Scan( &user.Role, &user.User, &user.Country, &user.Email, &user.Extent.X1, &user.Extent.Y1, &user.Extent.X2, &user.Extent.Y2, &user.Reports, &user.Active, ); err != nil { return } users = append(users, user) } jr = mw.JSONResult{ Result: struct { Users []*models.User `json:"users"` }{users}, } return } func listUser(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "error: user invalid", } return } result := &models.User{ User: user, Extent: &models.BoundingBox{}, } err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan( &result.Role, &result.Country, &result.Email, &result.Extent.X1, &result.Extent.Y1, &result.Extent.X2, &result.Extent.Y2, &result.Reports, ) switch { case err == sql.ErrNoRows: err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } return case err != nil: return } jr.Result = result return } func sendTestMail(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { err = mw.JSONError{ Code: http.StatusBadRequest, Message: "error: user invalid", } return } userData := &models.User{ User: user, Extent: &models.BoundingBox{}, } err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan( &userData.Role, &userData.Country, &userData.Email, &userData.Extent.X1, &userData.Extent.Y1, &userData.Extent.X2, &userData.Extent.Y2, &userData.Reports, ) switch { case err == sql.ErrNoRows: err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } return case err != nil: return } var subject string var tmplVars = struct { User string Timestamp string Email string }{ User: string(user), Timestamp: time.Now().Format("2006-01-02 15:04:05"), Email: string(userData.Email), } var bodyTmpl *template.Template switch userData.Role { case "sys_admin": subject = "Gemma: Sysadmin Notification TEST" bodyTmpl = testSysadminNotifyMailTmpl case "waterway_admin": subject = "Gemma: Waterway Admin Notification TEST" bodyTmpl = testWWAdminNotifyMailTmpl default: subject = "Gemma: Waterway User Notification TEST" bodyTmpl = testWWUserNotifyMailTmpl } var buf bytes.Buffer if err := bodyTmpl.Execute(&buf, &tmplVars); err != nil { log.Errorf("%v\n", err) } err = misc.SendMail(string(userData.Email), subject, buf.String()) if err != nil { return } jr.Result = &struct { Message string `json:"message"` }{"Sending test mail."} return }