Raw
 1package storage
 2
 3import (
 4	"context"
 5	"database/sql"
 6	"encoding/json"
 7	"errors"
 8	"fmt"
 9	"time"
10)
11
12var ErrPasswordChanged = errors.New("storage: the password hash changed since it was verified")
13
14func (s *Store) ChangePasswordAudited(ctx context.Context, userID, keepSessionID string, oldHash, newHash []byte, audit AppendAuditInput) (Revoked, error) {
15	return s.ChangePasswordAuditedAt(ctx, userID, keepSessionID, oldHash, newHash, audit, s.now())
16}
17
18func (s *Store) ChangePasswordAuditedAt(ctx context.Context, userID, keepSessionID string, oldHash, newHash []byte, audit AppendAuditInput, now time.Time) (Revoked, error) {
19	const operation = "change password"
20	if err := requireValue("kept session id", keepSessionID); err != nil {
21		return Revoked{}, invalid(operation, err)
22	}
23	if len(oldHash) == 0 || len(newHash) == 0 {
24		return Revoked{}, invalid(operation, fmt.Errorf("password hash is empty"))
25	}
26	fields := map[string]any{}
27	if audit.Detail != "" {
28		if err := json.Unmarshal([]byte(audit.Detail), &fields); err != nil || fields == nil {
29			return Revoked{}, invalid(operation, fmt.Errorf("audit detail is not a JSON object"))
30		}
31	}
32	var revoked Revoked
33	err := s.auditedAt(ctx, operation, audit.Actor, now, func(tx *sql.Tx, at time.Time) error {
34		var owner, username string
35		var dead, disabled int
36		var expires int64
37		err := tx.QueryRowContext(ctx, `
38SELECT f.user_id, f.revoked, f.expires_at, u.username, u.disabled
39FROM forward_sessions f JOIN users u ON u.id = f.user_id
40WHERE f.id = ?`, keepSessionID).Scan(&owner, &dead, &expires, &username, &disabled)
41		switch {
42		case errors.Is(err, sql.ErrNoRows):
43			return invalid(operation, fmt.Errorf("kept session does not exist"))
44		case err != nil:
45			return classify(operation, err)
46		case owner != userID:
47			return invalid(operation, fmt.Errorf("kept session does not belong to the user"))
48		case dead != 0 || expires <= timeToInt(at):
49			return invalid(operation, fmt.Errorf("kept session is not live"))
50		case disabled != 0:
51			return invalid(operation, fmt.Errorf("the user is disabled"))
52		}
53		changed, err := execCount(ctx, tx, operation, `UPDATE users SET password_hash = ? WHERE id = ? AND password_hash = ?`, newHash, userID, oldHash)
54		if err != nil {
55			return err
56		}
57		if changed == 0 {
58			return fmt.Errorf("%s: %w", operation, ErrPasswordChanged)
59		}
60		if revoked, err = revokeCredentials(ctx, tx, operation, userID, keepSessionID, true); err != nil {
61			return err
62		}
63		if _, err := endResetLink(ctx, tx, operation, userID); err != nil {
64			return err
65		}
66		fields["username"] = username
67		return writeAudit(ctx, tx, operation, audit.Actor, audit.Action, audit.Subject, at, revoked.into(fields))
68	})
69	if err != nil {
70		return Revoked{}, err
71	}
72	return revoked, nil
73}