view pkg/controllers/user.go @ 5250:13e1767b63a1 new-fwa

Reverted: 8c44b518141a, which didn't fix anything.
author Sascha Wilde <wilde@intevation.de>
date Tue, 12 May 2020 23:03:21 +0200
parents e020e6e34ad7
children b1b9b384540d
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"
	"log"
	"net/http"
	"text/template"
	"time"

	"github.com/gorilla/mux"

	"gemma.intevation.de/gemma/pkg/auth"
	"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)`
	createUserExtentSQL = `INSERT INTO users.list_users
  VALUES ($1, $2, $3, $4,
  ST_MakeBox2D(ST_Point($5, $6), ST_Point($7, $8)), $9)`

	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)
  = ($2, $3, $4, $5, NULL, $6)
  WHERE username = $1`
	updateUserExtentSQL = `UPDATE users.list_users
  SET (rolname, username, pw, country, map_extent, email_address)
  = ($2, $3, $4, $5, ST_MakeBox2D(ST_Point($6, $7), ST_Point($8, $9)), $10)
  WHERE username = $1`

	deleteUserSQL = `DELETE FROM users.list_users WHERE username = $1`

	listUsersSQL = `SELECT
  rolname,
  username,
  country,
  email_address,
  ST_XMin(map_extent), ST_YMin(map_extent),
  ST_XMax(map_extent), ST_YMax(map_extent)
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)
FROM users.list_users
WHERE username = $1`
)

var (
	testSysadminNotifyMailTmpl = template.Must(
		template.New("sysadmin").Parse(`Dear {{ .User }},

this is a test email for 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 send to
the address: {{ .Email }} with details on the error condition.`))

	testWWAdminNotifyMailTmpl = template.Must(
		template.New("waterwayadmin").Parse(`Dear {{ .User }},

this is a test email for the Gemma System Imports notification service.  You
recieved this mail, because a System Administrator triggered the test mail
sending function at {{ .Timestamp }}.

When the status of an data import managed by you changes an automated mail will
be send to the address: {{ .Email }} with details on the new import status
(inkluding import errors) and details on the concerned import.`))
)

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.Printf("error: %v\n", err2)
	}

	var res sql.Result

	if res, err = db.ExecContext(ctx, deleteUserSQL, user); 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
	}

	// Running in a go routine should not be necessary.
	go func() { auth.Sessions.Logout(user) }()

	jr = mw.JSONResult{Code: http.StatusNoContent}
	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,
			)
		} 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,
			)
		}
	} 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)) }()
	}

	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,
		)
	} 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,
		)
	}

	if err != nil {
		m, c := pgxutils.ReadableError{Err: err}.MessageAndCode()
		err = mw.JSONError{Code: c, Message: m}
		return
	}

	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,
		); 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,
	)

	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,
	)

	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
	if userData.Role == "sys_admin" {
		subject = "Gemma: Sysadmin Notification TEST"
		bodyTmpl = testSysadminNotifyMailTmpl
	} else if userData.Role == "waterway_admin" {
		subject = "Gemma: Waterway Admin Notification TEST"
		bodyTmpl = testWWAdminNotifyMailTmpl
	} else {
		err = mw.JSONError{
			Code:    http.StatusBadRequest,
			Message: "Test mails can only be generated for admin roles.",
		}
		return
	}
	var buf bytes.Buffer
	if err := bodyTmpl.Execute(&buf, &tmplVars); err != nil {
		log.Printf("error: %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
}