Raw
 1package session
 2
 3import (
 4	"context"
 5	"errors"
 6	"fmt"
 7
 8	"tug/internal/service/identity"
 9	"tug/internal/service/password"
10	"tug/internal/storage"
11)
12
13var (
14	ErrPasswordRejected     = errors.New("session: the current password is not correct")
15	ErrPasswordUnacceptable = errors.New("session: the new password is not acceptable")
16	ErrPasswordChanged      = errors.New("session: the password changed since it was verified")
17	ErrNoSuchSession        = errors.New("session: no such session of the user")
18	ErrDisplayNameInvalid   = errors.New("session: the display name is not valid")
19	ErrNoSuchSSHKey         = errors.New("session: no such SSH key of the user")
20)
21
22func (s *Service) ChangePassword(ctx context.Context, p Principal, current, next string) (int64, error) {
23	const operation = "change password"
24	if p.SessionID == "" {
25		return 0, fmt.Errorf("session: %s: %w: no current session to keep", operation, ErrNoSession)
26	}
27	user, err := s.identity.Authenticate(ctx, p.Username, current)
28	switch {
29	case errors.Is(err, identity.ErrUnavailable):
30		return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrUnavailable, err)
31	case err == nil && user.ID == p.UserID:
32	case err == nil, errors.Is(err, identity.ErrRejected):
33		if err := s.audit(ctx, auditInput(p.UserID, "password.change_refused", p.UserID, hostDetail{Host: p.Host})); err != nil {
34			return 0, fmt.Errorf("session: %s: the refusal was not audited: %w", operation, err)
35		}
36		return 0, fmt.Errorf("session: %s: %w", operation, ErrPasswordRejected)
37	default:
38		return 0, failure(operation, err)
39	}
40	if err := password.Acceptable(next); err != nil {
41		return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrPasswordUnacceptable, err)
42	}
43	hash, err := password.HashContext(ctx, next)
44	switch {
45	case errors.Is(err, password.ErrBusy):
46		return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrUnavailable, err)
47	case err != nil:
48		return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrPasswordUnacceptable, err)
49	}
50	revoked, err := s.store.ChangePasswordAuditedAt(ctx, p.UserID, p.SessionID, user.PasswordHash, hash, auditInput(p.UserID, "user.password_change", p.UserID, keptDetail{Kept: p.SessionID}), s.clock())
51	switch {
52	case errors.Is(err, storage.ErrPasswordChanged):
53		return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrPasswordChanged, err)
54	case errors.Is(err, storage.ErrInvalid):
55		return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrNoSession, err)
56	case err != nil:
57		return 0, failure(operation, err)
58	}
59	return revoked.Sessions, nil
60}
61
62func (s *Service) RevokeOwn(ctx context.Context, p Principal, sessionID string) error {
63	const operation = "revoke own"
64	row, err := s.store.ForwardSessionByID(ctx, sessionID)
65	switch {
66	case errors.Is(err, storage.ErrNotFound), err == nil && (row.UserID != p.UserID || row.Revoked):
67		return fmt.Errorf("session: %s: %w", operation, ErrNoSuchSession)
68	case err != nil:
69		return failure(operation, err)
70	}
71	err = s.store.RevokeOwnForwardSessionAuditedAt(ctx, p.UserID, sessionID, auditInput(p.UserID, "session.revoke", sessionID, hostDetail{Host: row.AppDomain}), s.clock())
72	switch {
73	case errors.Is(err, storage.ErrNotFound):
74		return fmt.Errorf("session: %s: %w", operation, ErrNoSuchSession)
75	case err != nil:
76		return failure(operation, err)
77	}
78	return nil
79}
80
81func (s *Service) UpdateDisplayName(ctx context.Context, p Principal, name string) error {
82	const operation = "update display name"
83	if err := storage.CheckDisplayName(name); err != nil {
84		return fmt.Errorf("session: %s: %w: %w", operation, ErrDisplayNameInvalid, err)
85	}
86	if err := s.store.UpdateOwnDisplayNameAuditedAt(ctx, p.UserID, name, storage.AppendAuditInput{Actor: p.UserID, Action: "user.update", Subject: p.UserID}, s.clock()); err != nil {
87		return failure(operation, err)
88	}
89	return nil
90}