Jan Dittberner
368bd8eefb
- remove the old code and its dependencies - perform some refactoring and fix notifications - add TODO tags for observed shortcomings - rename voters.go to users.go - implement health check for SMTP connection
324 lines
7.1 KiB
Go
324 lines
7.1 KiB
Go
/*
|
|
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"
|
|
)
|
|
|
|
type Role struct {
|
|
Name string `db:"role"`
|
|
}
|
|
|
|
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:"-"`
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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) (*User, error) {
|
|
row := m.DB.QueryRowxContext(
|
|
ctx,
|
|
`SELECT DISTINCT v.id, v.name
|
|
FROM voters v
|
|
JOIN user_roles ur ON v.id = ur.voter_id
|
|
WHERE v.id = ?
|
|
AND ur.role = ?`,
|
|
voterID,
|
|
RoleVoter,
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
return &user, nil
|
|
}
|