package session import ( "context" "errors" "fmt" "tug/internal/service/identity" "tug/internal/service/password" "tug/internal/storage" ) var ( ErrPasswordRejected = errors.New("session: the current password is not correct") ErrPasswordUnacceptable = errors.New("session: the new password is not acceptable") ErrPasswordChanged = errors.New("session: the password changed since it was verified") ErrNoSuchSession = errors.New("session: no such session of the user") ErrDisplayNameInvalid = errors.New("session: the display name is not valid") ErrNoSuchSSHKey = errors.New("session: no such SSH key of the user") ) func (s *Service) ChangePassword(ctx context.Context, p Principal, current, next string) (int64, error) { const operation = "change password" if p.SessionID == "" { return 0, fmt.Errorf("session: %s: %w: no current session to keep", operation, ErrNoSession) } user, err := s.identity.Authenticate(ctx, p.Username, current) switch { case errors.Is(err, identity.ErrUnavailable): return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrUnavailable, err) case err == nil && user.ID == p.UserID: case err == nil, errors.Is(err, identity.ErrRejected): if err := s.audit(ctx, auditInput(p.UserID, "password.change_refused", p.UserID, hostDetail{Host: p.Host})); err != nil { return 0, fmt.Errorf("session: %s: the refusal was not audited: %w", operation, err) } return 0, fmt.Errorf("session: %s: %w", operation, ErrPasswordRejected) default: return 0, failure(operation, err) } if err := password.Acceptable(next); err != nil { return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrPasswordUnacceptable, err) } hash, err := password.HashContext(ctx, next) switch { case errors.Is(err, password.ErrBusy): return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrUnavailable, err) case err != nil: return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrPasswordUnacceptable, err) } 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()) switch { case errors.Is(err, storage.ErrPasswordChanged): return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrPasswordChanged, err) case errors.Is(err, storage.ErrInvalid): return 0, fmt.Errorf("session: %s: %w: %w", operation, ErrNoSession, err) case err != nil: return 0, failure(operation, err) } return revoked.Sessions, nil } func (s *Service) RevokeOwn(ctx context.Context, p Principal, sessionID string) error { const operation = "revoke own" row, err := s.store.ForwardSessionByID(ctx, sessionID) switch { case errors.Is(err, storage.ErrNotFound), err == nil && (row.UserID != p.UserID || row.Revoked): return fmt.Errorf("session: %s: %w", operation, ErrNoSuchSession) case err != nil: return failure(operation, err) } err = s.store.RevokeOwnForwardSessionAuditedAt(ctx, p.UserID, sessionID, auditInput(p.UserID, "session.revoke", sessionID, hostDetail{Host: row.AppDomain}), s.clock()) switch { case errors.Is(err, storage.ErrNotFound): return fmt.Errorf("session: %s: %w", operation, ErrNoSuchSession) case err != nil: return failure(operation, err) } return nil } func (s *Service) UpdateDisplayName(ctx context.Context, p Principal, name string) error { const operation = "update display name" if err := storage.CheckDisplayName(name); err != nil { return fmt.Errorf("session: %s: %w: %w", operation, ErrDisplayNameInvalid, err) } if err := s.store.UpdateOwnDisplayNameAuditedAt(ctx, p.UserID, name, storage.AppendAuditInput{Actor: p.UserID, Action: "user.update", Subject: p.UserID}, s.clock()); err != nil { return failure(operation, err) } return nil }