|
|
|
/*
|
|
|
|
Copyright 2017-2022 CAcert Inc.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"golang.org/x/text/cases"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Role struct {
|
|
|
|
Name string `db:"role"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Role) String() string {
|
|
|
|
return cases.Title(language.BritishEnglish).String(strings.ToLower(r.Name))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Role) Scan(src any) error {
|
|
|
|
value, ok := src.(string)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("could not cast %v of %T to string", src, src)
|
|
|
|
}
|
|
|
|
|
|
|
|
*r = Role{Name: value}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type RoleName string
|
|
|
|
|
|
|
|
const (
|
|
|
|
RoleAdmin RoleName = "ADMIN"
|
|
|
|
RoleSecretary RoleName = "SECRETARY"
|
|
|
|
RoleVoter RoleName = "VOTER"
|
|
|
|
)
|
|
|
|
|
|
|
|
// The User type is used for mapping users from the voters table. The table
|
|
|
|
// has two columns that are obsolete:
|
|
|
|
//
|
|
|
|
// enabled -> replaced with user_roles where role is 'VOTER'
|
|
|
|
// reminder -> replaced with address from emails where reminder is true
|
|
|
|
//
|
|
|
|
// The columns cannot be dropped in SQLite without dropping and recreating
|
|
|
|
// the voters table and all foreign key indices pointing to that table.
|
|
|
|
type User struct {
|
|
|
|
ID int64 `db:"id"`
|
|
|
|
Name string `db:"name"`
|
|
|
|
Reminder string `db:"reminder"` // reminder email address
|
|
|
|
roles []*Role `db:"-"`
|
|
|
|
canDelete bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) Roles() ([]*Role, error) {
|
|
|
|
if u.roles != nil {
|
|
|
|
return u.roles, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, errors.New("call to Roles required")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) HasRole(roles ...RoleName) (bool, error) {
|
|
|
|
userRoles, err := u.Roles()
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
roleMatched := false
|
|
|
|
|
|
|
|
outer:
|
|
|
|
for _, role := range userRoles {
|
|
|
|
for _, checkRole := range roles {
|
|
|
|
if role.Name == string(checkRole) {
|
|
|
|
roleMatched = true
|
|
|
|
|
|
|
|
break outer
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return roleMatched, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *User) CanDelete() bool {
|
|
|
|
return u.canDelete
|
|
|
|
}
|
|
|
|
|
|
|
|
type UserModel struct {
|
|
|
|
DB *sqlx.DB
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) ReminderVoters(ctx context.Context) ([]*User, error) {
|
|
|
|
rows, err := m.DB.QueryxContext(
|
|
|
|
ctx,
|
|
|
|
`SELECT v.id, v.name, e.address AS reminder
|
|
|
|
FROM voters v
|
|
|
|
JOIN emails e ON v.id = e.voter
|
|
|
|
JOIN user_roles ur ON v.id = ur.voter_id
|
|
|
|
WHERE ur.role = ?
|
|
|
|
AND e.reminder = TRUE`,
|
|
|
|
RoleVoter,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not run query: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
result := make([]*User, 0)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not fetch row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
|
|
|
if err := rows.StructScan(&user); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not scan row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
result = append(result, &user)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) ByEmails(ctx context.Context, emails []string) (*User, error) {
|
|
|
|
for i := range emails {
|
|
|
|
emails[i] = strings.ToLower(emails[i])
|
|
|
|
}
|
|
|
|
|
|
|
|
query, args, err := sqlx.In(
|
|
|
|
`WITH reminders AS (SELECT voter, address
|
|
|
|
FROM emails
|
|
|
|
WHERE reminder = TRUE)
|
|
|
|
SELECT DISTINCT v.id, v.name, reminders.address AS reminder
|
|
|
|
FROM voters v
|
|
|
|
JOIN emails e ON e.voter = v.id
|
|
|
|
LEFT JOIN reminders ON v.id = reminders.voter
|
|
|
|
WHERE e.address IN (?)`, emails)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not build query: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not run query: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
var (
|
|
|
|
user User
|
|
|
|
count int
|
|
|
|
)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
count++
|
|
|
|
|
|
|
|
if count > 1 {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"multiple voters found for addresses in certificate %s",
|
|
|
|
strings.Join(emails, ", "),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not fetch row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := rows.StructScan(&user); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not get user from row: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if user.roles, err = m.Roles(ctx, &user); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not retrieve roles for user %s: %w", user.Name, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) Roles(ctx context.Context, user *User) ([]*Role, error) {
|
|
|
|
rows, err := m.DB.QueryxContext(ctx, `SELECT role FROM user_roles WHERE voter_id=?`, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not query roles for %s: %w", user.Name, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
result := make([]*Role, 0)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not retrieve row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var role Role
|
|
|
|
|
|
|
|
if err := rows.StructScan(&role); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not get role from row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
result = append(result, &role)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) Create(ctx context.Context, name string, reminder string, emails []string) (int64, error) {
|
|
|
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("could not start transaction: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(tx *sqlx.Tx) {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
}(tx)
|
|
|
|
|
|
|
|
res, err := tx.ExecContext(
|
|
|
|
ctx,
|
|
|
|
`INSERT INTO voters (name, enabled) VALUES (?, 0)`,
|
|
|
|
name)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("could not insert user: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
userID, err := res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("could not get user id: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range emails {
|
|
|
|
_, err := tx.ExecContext(
|
|
|
|
ctx,
|
|
|
|
`INSERT INTO emails (voter, address, reminder)
|
|
|
|
VALUES (?, ?, ?)`,
|
|
|
|
userID,
|
|
|
|
emails[i],
|
|
|
|
emails[i] == reminder,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("could not insert email %s for voter %s: %w", emails[i], name, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
return 0, fmt.Errorf("could not commit user transaction: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return userID, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) InRole(ctx context.Context, role RoleName) ([]*User, error) {
|
|
|
|
rows, err := m.DB.QueryxContext(
|
|
|
|
ctx,
|
|
|
|
`SELECT voters.id, voters.name
|
|
|
|
FROM voters
|
|
|
|
JOIN user_roles ur ON voters.id = ur.voter_id
|
|
|
|
WHERE ur.role = ?
|
|
|
|
ORDER BY voters.name`,
|
|
|
|
role,
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not execute query: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
result := make([]*User, 0)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not get row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
|
|
|
if err := rows.StructScan(&user); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not scan row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
result = append(result, &user)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) Voters(ctx context.Context) ([]*User, error) {
|
|
|
|
return m.InRole(ctx, RoleVoter)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) ByID(ctx context.Context, voterID int64, options ...UserListOption) (*User, error) {
|
|
|
|
row := m.DB.QueryRowxContext(
|
|
|
|
ctx,
|
|
|
|
`SELECT DISTINCT v.id, v.name, e.address AS reminder
|
|
|
|
FROM voters v
|
|
|
|
LEFT OUTER JOIN emails e ON e.voter = v.id
|
|
|
|
WHERE v.id = ?
|
|
|
|
AND e.reminder = TRUE
|
|
|
|
`,
|
|
|
|
voterID,
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := row.Err(); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not execute query: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var user User
|
|
|
|
|
|
|
|
if err := row.StructScan(&user); err != nil {
|
|
|
|
return nil, fmt.Errorf("could not scan row: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, option := range options {
|
|
|
|
if err := option(ctx, []*User{&user}); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type UserListOption func(context.Context, []*User) error
|
|
|
|
|
|
|
|
func (m *UserModel) List(ctx context.Context, options ...UserListOption) ([]*User, error) {
|
|
|
|
rows, err := m.DB.QueryxContext(ctx, `SELECT id, name FROM voters ORDER BY name`)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errCouldNotExecuteQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
users := make([]*User, 0)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
return nil, errCouldNotFetchRow(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var user User
|
|
|
|
if err = rows.StructScan(&user); err != nil {
|
|
|
|
return nil, errCouldNotScanResult(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
users = append(users, &user)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, option := range options {
|
|
|
|
if err = option(ctx, users); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return users, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) WithRolesOption() UserListOption {
|
|
|
|
return func(ctx context.Context, users []*User) error {
|
|
|
|
if len(users) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
userIDs := make([]int64, len(users))
|
|
|
|
for idx := range users {
|
|
|
|
userIDs[idx] = users[idx].ID
|
|
|
|
}
|
|
|
|
|
|
|
|
query, args, err := sqlx.In(
|
|
|
|
`SELECT voter_id, role FROM user_roles WHERE voter_id IN (?)`,
|
|
|
|
userIDs,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return errCouldNotCreateInQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
|
|
|
if err != nil {
|
|
|
|
return errCouldNotExecuteQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
roleMap, err := buildRoleMap(rows, len(users))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for idx := range users {
|
|
|
|
if roles, ok := roleMap[users[idx].ID]; ok {
|
|
|
|
users[idx].roles = roles
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
users[idx].roles = make([]*Role, 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildRoleMap(rows *sqlx.Rows, userCount int) (map[int64][]*Role, error) {
|
|
|
|
roleMap := make(map[int64][]*Role, userCount)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return nil, errCouldNotFetchRow(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
userID int64
|
|
|
|
role Role
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := rows.Scan(&userID, &role); err != nil {
|
|
|
|
return nil, errCouldNotScanResult(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := roleMap[userID]; !ok {
|
|
|
|
roleMap[userID] = []*Role{&role}
|
|
|
|
} else {
|
|
|
|
roleMap[userID] = append(roleMap[userID], &role)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return roleMap, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) CanDeleteOption() UserListOption {
|
|
|
|
return func(ctx context.Context, users []*User) error {
|
|
|
|
if len(users) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
userIDs := make([]int64, len(users))
|
|
|
|
for idx := range users {
|
|
|
|
userIDs[idx] = users[idx].ID
|
|
|
|
}
|
|
|
|
|
|
|
|
query, args, err := sqlx.In(
|
|
|
|
`SELECT id
|
|
|
|
FROM voters
|
|
|
|
WHERE id IN (?)
|
|
|
|
AND NOT EXISTS(SELECT * FROM decisions WHERE proponent = voters.id)
|
|
|
|
AND NOT EXISTS(SELECT * FROM votes WHERE voter = voters.id)
|
|
|
|
AND NOT EXISTS(SELECT * FROM user_roles WHERE voter_id = voters.id)`,
|
|
|
|
userIDs,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return errCouldNotCreateInQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
|
|
|
if err != nil {
|
|
|
|
return errCouldNotExecuteQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(rows *sqlx.Rows) {
|
|
|
|
_ = rows.Close()
|
|
|
|
}(rows)
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return errCouldNotFetchRow(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var userID int64
|
|
|
|
|
|
|
|
if err := rows.Scan(&userID); err != nil {
|
|
|
|
return errCouldNotScanResult(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for idx := range users {
|
|
|
|
if userID == users[idx].ID {
|
|
|
|
users[idx].canDelete = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *UserModel) DeleteUser(ctx context.Context, userToDelete, admin *User, reasoning string) error {
|
|
|
|
userInfo := struct {
|
|
|
|
Name string `json:"user"`
|
|
|
|
Address string `json:"address"`
|
|
|
|
}{Name: userToDelete.Name, Address: userToDelete.Reminder}
|
|
|
|
details := struct {
|
|
|
|
User any `json:"user"`
|
|
|
|
}{User: userInfo}
|
|
|
|
|
|
|
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
|
|
|
if err != nil {
|
|
|
|
return errCouldNotStartTransaction(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func(tx *sqlx.Tx) {
|
|
|
|
_ = tx.Rollback()
|
|
|
|
}(tx)
|
|
|
|
|
|
|
|
if _, err = tx.ExecContext(ctx, `DELETE FROM emails WHERE voter=?`, userToDelete.ID); err != nil {
|
|
|
|
return errCouldNotExecuteQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = tx.ExecContext(ctx, `DELETE FROM voters WHERE id=?`, userToDelete.ID); err != nil {
|
|
|
|
return errCouldNotExecuteQuery(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = AuditLog(ctx, tx, admin, AuditDeleteUser, reasoning, details); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
return errCouldNotCommitTransaction(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|