view pkg/controllers/user.go @ 4723:baabc2b2f094

Avoid creating user profiles without matching role The INSTEAD OF triggers on users.list_users did that already, but profile data coming e.g. via restoring a dump had been added also if there was no matching database role in the cluster. This also unifies the errors occuring on creation of users with existing role names that differed between roles with and without profile before. Note this is no referential integrity. A dropped role still leaves an orphaned profile behind.
author Tom Gottfried <tom@intevation.de>
date Thu, 17 Oct 2019 18:56:59 +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
}