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}