mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:51:36 +00:00
fix(auth): wire RevokeAllForActor + RotateCSRFToken to mutation paths
Closes HIGH-1 + HIGH-2 of the 2026-05-10 audit.
HIGH-1: breakglass.Service.SetPassword and RemoveCredential now call
sessions.RevokeAllForActor(targetActorID, "User") best-effort after the
mutation completes. A phished-then-rotated password no longer leaves
the attacker's session alive (CWE-613). Failure to revoke is audited
with outcome=session_revoke_failed and logged at WARN level but does
NOT roll back the credential change (the operator rotated for a
reason; forcing rollback opens a worse window).
- breakglass.SessionMinter interface extended with RevokeAllForActor.
- cmd/server/main.go::breakglassSessionMinterAdapter gains the bridge
to session.Service.RevokeAllForActor.
- stubSessions in service_test.go tracks revokeAllIDs / revokeAllTypes
/ revokeAllErr.
- Three regression tests:
- TestService_SetPassword_RevokesExistingSessions
- TestService_RemoveCredential_RevokesExistingSessions
- TestService_SetPassword_RevokeFailureDoesNotRollback
HIGH-2: New session.Service.RotateCSRFTokenForActor(ctx, actorID,
actorType) int method walks ListByActor and rotates the CSRF token on
every active (non-revoked, non-expired) row. Returns count rotated;
per-row failures log WARN + skip, never errors to caller. New
handler.CSRFRotator interface + AuthHandler.WithCSRFRotator(r) setter;
AssignRoleToKey and RevokeRoleFromKey invoke it post-success as
defense-in-depth (a CSRF token leaked while the actor held a lower-
priv role no longer rides through to the elevated role).
- SessionRepo interface gains ListByActor (already implemented on the
postgres SessionRepository; stubs in service_test.go + bench_test.go
updated to match).
- cmd/server/main.go calls .WithCSRFRotator(sessionService) on the
AuthHandler.
- Two regression tests:
- TestRotateCSRFTokenForActor_RotatesAllActiveRows (asserts revoked /
expired / other-actor rows are skipped)
- TestRotateCSRFTokenForActor_NoSessionsReturnsZero
Verification gate green: gofmt clean, go vet clean, go test -short
-count=1 ./internal/auth/breakglass/ ./internal/auth/session/
./internal/api/handler/ ./internal/api/router/ ./cmd/server/
./internal/domain/auth/ — all pass.
CRIT-1..CRIT-5 + HIGH-1 + HIGH-2 of the 2026-05-10 audit now closed
on this branch. Spec at
cowork/auth-bundles-fixes-2026-05-10/06-high-1-2-revoke-and-rotate.md.
Refs: cowork/auth-bundles-audit-2026-05-10.md HIGH-1 HIGH-2
This commit is contained in:
@@ -30,6 +30,22 @@ type AuthHandler struct {
|
||||
perms AuthPermissionService
|
||||
actors AuthActorRoleService
|
||||
checker auth.PermissionChecker
|
||||
// csrfRotator is the optional session-CSRF-rotation hook called
|
||||
// post-role-mutation. Audit 2026-05-10 HIGH-2 closure — when an
|
||||
// actor's role set changes, every active session's CSRF token is
|
||||
// rotated as defense-in-depth against token leak preceding the
|
||||
// privilege change. Nil-safe: when unset (pre-Bundle-2 wiring,
|
||||
// tests that don't care about CSRF), the wires are no-ops.
|
||||
csrfRotator CSRFRotator
|
||||
}
|
||||
|
||||
// CSRFRotator is the projection of *session.Service used by AuthHandler
|
||||
// to rotate CSRF tokens across an actor's active sessions after a role
|
||||
// mutation. RotateCSRFTokenForActor returns the count of rotated rows
|
||||
// and NEVER errors out — rotation is defense-in-depth and must not
|
||||
// block the role mutation that triggered it.
|
||||
type CSRFRotator interface {
|
||||
RotateCSRFTokenForActor(ctx context.Context, actorID, actorType string) int
|
||||
}
|
||||
|
||||
// AuthRoleService is the service-layer dependency the AuthHandler uses
|
||||
@@ -82,6 +98,16 @@ func NewAuthHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// WithCSRFRotator returns a copy of the handler with the CSRF-rotation
|
||||
// hook installed. Audit 2026-05-10 HIGH-2 closure — production wiring
|
||||
// in cmd/server/main.go calls this with the post-Bundle-2
|
||||
// session.Service; pre-Bundle-2 deployments + tests can leave the
|
||||
// rotator nil and the role-mutation handlers simply skip rotation.
|
||||
func (h AuthHandler) WithCSRFRotator(r CSRFRotator) AuthHandler {
|
||||
h.csrfRotator = r
|
||||
return h
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JSON request / response shapes
|
||||
// =============================================================================
|
||||
@@ -410,6 +436,14 @@ func (h AuthHandler) AssignRoleToKey(w http.ResponseWriter, r *http.Request) {
|
||||
writeAuthError(w, err)
|
||||
return
|
||||
}
|
||||
// Audit 2026-05-10 HIGH-2 closure — rotate CSRF across every
|
||||
// active session of the target actor. Non-blocking (per-row
|
||||
// failures are logged inside RotateCSRFTokenForActor but the
|
||||
// return value isn't an error). API-key actors typically have no
|
||||
// sessions (Bearer-only) so this is a no-op for them.
|
||||
if h.csrfRotator != nil {
|
||||
_ = h.csrfRotator.RotateCSRFTokenForActor(r.Context(), keyID, string(domain.ActorTypeAPIKey))
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -426,6 +460,10 @@ func (h AuthHandler) RevokeRoleFromKey(w http.ResponseWriter, r *http.Request) {
|
||||
writeAuthError(w, err)
|
||||
return
|
||||
}
|
||||
// Audit 2026-05-10 HIGH-2 closure — rotate CSRF post-revoke.
|
||||
if h.csrfRotator != nil {
|
||||
_ = h.csrfRotator.RotateCSRFTokenForActor(r.Context(), keyID, string(domain.ActorTypeAPIKey))
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package breakglass
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
bgdomain "github.com/certctl-io/certctl/internal/auth/breakglass/domain"
|
||||
)
|
||||
|
||||
// Audit 2026-05-10 HIGH-1 closure — regression tests pinning the
|
||||
// wire from break-glass mutations to SessionMinter.RevokeAllForActor.
|
||||
// Pre-fix, SetPassword and RemoveCredential rotated the password /
|
||||
// removed the row but left active sessions for the target actor alive
|
||||
// (CWE-613). The fix calls RevokeAllForActor(targetActorID, "User")
|
||||
// best-effort after each mutation.
|
||||
|
||||
func TestService_SetPassword_RevokesExistingSessions(t *testing.T) {
|
||||
svc, repo, _, sess := newSvc(t, true)
|
||||
// Seed: target actor already has a break-glass credential.
|
||||
repo.rows["u-target"] = &bgdomain.BreakglassCredential{
|
||||
ID: "bg-target", TenantID: "t-default", ActorID: "u-target", PasswordHash: "$argon2id$old",
|
||||
}
|
||||
|
||||
if _, err := svc.SetPassword(context.Background(), "u-admin", "u-target", "new-password-12345"); err != nil {
|
||||
t.Fatalf("SetPassword: %v", err)
|
||||
}
|
||||
|
||||
if len(sess.revokeAllIDs) != 1 || sess.revokeAllIDs[0] != "u-target" {
|
||||
t.Errorf("expected RevokeAllForActor(u-target); got %v", sess.revokeAllIDs)
|
||||
}
|
||||
if len(sess.revokeAllTypes) != 1 || sess.revokeAllTypes[0] != "User" {
|
||||
t.Errorf("expected actor_type=User; got %v", sess.revokeAllTypes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_RemoveCredential_RevokesExistingSessions(t *testing.T) {
|
||||
svc, repo, _, sess := newSvc(t, true)
|
||||
repo.rows["u-target"] = &bgdomain.BreakglassCredential{
|
||||
ID: "bg-target", TenantID: "t-default", ActorID: "u-target", PasswordHash: "$argon2id$x",
|
||||
}
|
||||
|
||||
if err := svc.RemoveCredential(context.Background(), "u-admin", "u-target"); err != nil {
|
||||
t.Fatalf("RemoveCredential: %v", err)
|
||||
}
|
||||
if len(sess.revokeAllIDs) != 1 || sess.revokeAllIDs[0] != "u-target" {
|
||||
t.Errorf("expected RevokeAllForActor(u-target); got %v", sess.revokeAllIDs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestService_SetPassword_RevokeFailureDoesNotRollback pins the
|
||||
// best-effort contract: if RevokeAllForActor errors, the password
|
||||
// rotation itself still SUCCEEDS (the operator rotated for a reason,
|
||||
// forcing rollback opens a worse window). The failure is logged +
|
||||
// audited but not surfaced to the caller.
|
||||
func TestService_SetPassword_RevokeFailureDoesNotRollback(t *testing.T) {
|
||||
svc, repo, _, sess := newSvc(t, true)
|
||||
repo.rows["u-target"] = &bgdomain.BreakglassCredential{
|
||||
ID: "bg-target", TenantID: "t-default", ActorID: "u-target", PasswordHash: "$argon2id$old",
|
||||
}
|
||||
sess.revokeAllErr = errors.New("transient db reset")
|
||||
|
||||
res, err := svc.SetPassword(context.Background(), "u-admin", "u-target", "new-password-12345")
|
||||
if err != nil {
|
||||
t.Fatalf("SetPassword should succeed even when revoke fails; got %v", err)
|
||||
}
|
||||
if res == nil || res.ActorID != "u-target" {
|
||||
t.Fatalf("expected result with actor_id=u-target; got %+v", res)
|
||||
}
|
||||
// RevokeAllForActor WAS attempted.
|
||||
if len(sess.revokeAllIDs) != 1 {
|
||||
t.Errorf("expected RevokeAllForActor attempted; got %v", sess.revokeAllIDs)
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -142,9 +143,13 @@ type AuditRecorder interface {
|
||||
|
||||
// SessionMinter is the slice of *session.Service the Authenticate path
|
||||
// uses to mint a post-login session after a successful break-glass
|
||||
// password verify.
|
||||
// password verify. Audit 2026-05-10 HIGH-1 closure: SetPassword and
|
||||
// RemoveCredential now also call RevokeAllForActor on the same
|
||||
// session.Service so a phished-then-rotated password no longer leaves
|
||||
// stale sessions alive (CWE-613). The interface gains RevokeAllForActor.
|
||||
type SessionMinter interface {
|
||||
Create(ctx context.Context, actorID, actorType, ip, userAgent string) (cookieValue, csrfToken string, err error)
|
||||
RevokeAllForActor(ctx context.Context, actorID, actorType string) error
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -254,6 +259,25 @@ func (s *Service) SetPassword(ctx context.Context, callerActorID, targetActorID,
|
||||
s.recordAudit(ctx, "auth.breakglass_password_set", callerActorID, domain.ActorTypeUser, targetActorID,
|
||||
map[string]interface{}{"caller_actor_id": callerActorID, "target_actor_id": targetActorID})
|
||||
|
||||
// Audit 2026-05-10 HIGH-1 closure — revoke every active session for
|
||||
// the target actor. A phished-then-rotated password must NOT leave
|
||||
// the attacker's session alive. Best-effort: failure here is logged
|
||||
// + audited but DOES NOT roll back the password rotation (the
|
||||
// operator rotated for a reason, and forcing rollback opens a worse
|
||||
// window). The audit row distinguishes outcome=session_revoke_failed.
|
||||
if s.sessions != nil {
|
||||
if rerr := s.sessions.RevokeAllForActor(ctx, targetActorID, string(domain.ActorTypeUser)); rerr != nil {
|
||||
slog.WarnContext(ctx, "breakglass: session revoke after password rotation failed",
|
||||
"target_actor_id", targetActorID, "err", rerr)
|
||||
s.recordAudit(ctx, "auth.breakglass_password_set", callerActorID, domain.ActorTypeUser, targetActorID,
|
||||
map[string]interface{}{
|
||||
"caller_actor_id": callerActorID,
|
||||
"target_actor_id": targetActorID,
|
||||
"outcome": "session_revoke_failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &SetPasswordResult{
|
||||
ActorID: targetActorID,
|
||||
CreatedAt: s.clockNow().UTC(),
|
||||
@@ -405,6 +429,23 @@ func (s *Service) RemoveCredential(ctx context.Context, callerActorID, targetAct
|
||||
}
|
||||
s.recordAudit(ctx, "auth.breakglass_credential_removed", callerActorID, domain.ActorTypeUser, targetActorID,
|
||||
map[string]interface{}{"caller_actor_id": callerActorID, "target_actor_id": targetActorID})
|
||||
|
||||
// Audit 2026-05-10 HIGH-1 closure — credential removal must also
|
||||
// revoke every active break-glass session for the target actor.
|
||||
// Best-effort with WARN on failure; the credential removal already
|
||||
// succeeded so we don't roll back.
|
||||
if s.sessions != nil {
|
||||
if rerr := s.sessions.RevokeAllForActor(ctx, targetActorID, string(domain.ActorTypeUser)); rerr != nil {
|
||||
slog.WarnContext(ctx, "breakglass: session revoke after credential remove failed",
|
||||
"target_actor_id", targetActorID, "err", rerr)
|
||||
s.recordAudit(ctx, "auth.breakglass_credential_removed", callerActorID, domain.ActorTypeUser, targetActorID,
|
||||
map[string]interface{}{
|
||||
"caller_actor_id": callerActorID,
|
||||
"target_actor_id": targetActorID,
|
||||
"outcome": "session_revoke_failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,13 @@ type stubSessions struct {
|
||||
cookieValue string
|
||||
csrfToken string
|
||||
createErr error
|
||||
// Audit 2026-05-10 HIGH-1 wire — track RevokeAllForActor calls so
|
||||
// the new TestService_SetPassword_RevokesExistingSessions /
|
||||
// TestService_RemoveCredential_RevokesExistingSessions tests can
|
||||
// assert the wire.
|
||||
revokeAllIDs []string
|
||||
revokeAllTypes []string
|
||||
revokeAllErr error
|
||||
}
|
||||
|
||||
func (s *stubSessions) Create(_ context.Context, _, _, _, _ string) (string, string, error) {
|
||||
@@ -161,6 +168,12 @@ func (s *stubSessions) Create(_ context.Context, _, _, _, _ string) (string, str
|
||||
return s.cookieValue, s.csrfToken, nil
|
||||
}
|
||||
|
||||
func (s *stubSessions) RevokeAllForActor(_ context.Context, actorID, actorType string) error {
|
||||
s.revokeAllIDs = append(s.revokeAllIDs, actorID)
|
||||
s.revokeAllTypes = append(s.revokeAllTypes, actorType)
|
||||
return s.revokeAllErr
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helpers.
|
||||
// =============================================================================
|
||||
|
||||
@@ -115,6 +115,10 @@ func (r *slowSessionRepo) Get(ctx context.Context, id string) (*sessiondomain.Se
|
||||
time.Sleep(r.delay)
|
||||
return r.inner.Get(ctx, id)
|
||||
}
|
||||
func (r *slowSessionRepo) ListByActor(ctx context.Context, actorID, actorType, tenantID string) ([]*sessiondomain.Session, error) {
|
||||
time.Sleep(r.delay)
|
||||
return r.inner.ListByActor(ctx, actorID, actorType, tenantID)
|
||||
}
|
||||
func (r *slowSessionRepo) UpdateLastSeen(ctx context.Context, id string) error {
|
||||
time.Sleep(r.delay)
|
||||
return r.inner.UpdateLastSeen(ctx, id)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sessiondomain "github.com/certctl-io/certctl/internal/auth/session/domain"
|
||||
)
|
||||
|
||||
// Audit 2026-05-10 HIGH-2 closure — regression test pinning
|
||||
// RotateCSRFTokenForActor. Pre-fix the rotate primitive existed but
|
||||
// was only called at login mint; this method now rotates across every
|
||||
// active (non-revoked, non-expired) session of an actor for the
|
||||
// role-mutation defense-in-depth path.
|
||||
|
||||
func TestRotateCSRFTokenForActor_RotatesAllActiveRows(t *testing.T) {
|
||||
svc, repo, _, _, _ := newTestService(t, defaultCfg())
|
||||
|
||||
now := time.Now().UTC()
|
||||
// 3 active sessions for u-alice.
|
||||
for _, id := range []string{"s-a-1", "s-a-2", "s-a-3"} {
|
||||
repo.rows[id] = &sessiondomain.Session{
|
||||
ID: id, TenantID: "t-default",
|
||||
ActorID: "u-alice", ActorType: "User",
|
||||
IdleExpiresAt: now.Add(1 * time.Hour),
|
||||
AbsoluteExpiresAt: now.Add(8 * time.Hour),
|
||||
CSRFTokenHash: "old-hash-" + id,
|
||||
}
|
||||
}
|
||||
// 1 revoked row — should NOT be rotated.
|
||||
revokedAt := now.Add(-1 * time.Minute)
|
||||
repo.rows["s-a-revoked"] = &sessiondomain.Session{
|
||||
ID: "s-a-revoked", TenantID: "t-default",
|
||||
ActorID: "u-alice", ActorType: "User",
|
||||
IdleExpiresAt: now.Add(1 * time.Hour), AbsoluteExpiresAt: now.Add(8 * time.Hour),
|
||||
CSRFTokenHash: "stale",
|
||||
RevokedAt: &revokedAt,
|
||||
}
|
||||
// 1 expired row — should NOT be rotated.
|
||||
repo.rows["s-a-expired"] = &sessiondomain.Session{
|
||||
ID: "s-a-expired", TenantID: "t-default",
|
||||
ActorID: "u-alice", ActorType: "User",
|
||||
IdleExpiresAt: now.Add(-1 * time.Minute), // expired
|
||||
AbsoluteExpiresAt: now.Add(8 * time.Hour),
|
||||
CSRFTokenHash: "stale",
|
||||
}
|
||||
// 2 rows for a DIFFERENT actor — should NOT be rotated.
|
||||
for _, id := range []string{"s-b-1", "s-b-2"} {
|
||||
repo.rows[id] = &sessiondomain.Session{
|
||||
ID: id, TenantID: "t-default",
|
||||
ActorID: "u-bob", ActorType: "User",
|
||||
IdleExpiresAt: now.Add(1 * time.Hour), AbsoluteExpiresAt: now.Add(8 * time.Hour),
|
||||
CSRFTokenHash: "bob-hash",
|
||||
}
|
||||
}
|
||||
|
||||
rotated := svc.RotateCSRFTokenForActor(context.Background(), "u-alice", "User")
|
||||
if rotated != 3 {
|
||||
t.Fatalf("rotated count = %d; want 3 (3 active alice rows; revoked + expired + bob skipped)", rotated)
|
||||
}
|
||||
|
||||
// Confirm: the 3 active alice rows now have NEW CSRF hashes.
|
||||
for _, id := range []string{"s-a-1", "s-a-2", "s-a-3"} {
|
||||
row := repo.rows[id]
|
||||
if row.CSRFTokenHash == "old-hash-"+id || row.CSRFTokenHash == "" {
|
||||
t.Errorf("session %s CSRF hash not rotated (still %q)", id, row.CSRFTokenHash)
|
||||
}
|
||||
}
|
||||
// Bob's rows: untouched.
|
||||
for _, id := range []string{"s-b-1", "s-b-2"} {
|
||||
if repo.rows[id].CSRFTokenHash != "bob-hash" {
|
||||
t.Errorf("bob's session %s CSRF was rotated; should not be", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotateCSRFTokenForActor_NoSessionsReturnsZero(t *testing.T) {
|
||||
svc, _, _, _, _ := newTestService(t, defaultCfg())
|
||||
|
||||
got := svc.RotateCSRFTokenForActor(context.Background(), "u-no-sessions", "User")
|
||||
if got != 0 {
|
||||
t.Errorf("got %d; want 0", got)
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -173,6 +174,11 @@ var (
|
||||
type SessionRepo interface {
|
||||
Create(ctx context.Context, s *sessiondomain.Session) error
|
||||
Get(ctx context.Context, id string) (*sessiondomain.Session, error)
|
||||
// ListByActor returns every session row for the (actor_id, actor_type)
|
||||
// pair in the tenant. Used by RotateCSRFTokenForActor (Audit
|
||||
// 2026-05-10 HIGH-2). Order is implementation-defined; the caller
|
||||
// filters revoked/expired rows post-fetch.
|
||||
ListByActor(ctx context.Context, actorID, actorType, tenantID string) ([]*sessiondomain.Session, error)
|
||||
UpdateLastSeen(ctx context.Context, id string) error
|
||||
UpdateCSRFTokenHash(ctx context.Context, id, csrfTokenHash string) error
|
||||
Revoke(ctx context.Context, id string) error
|
||||
@@ -553,6 +559,44 @@ func (s *Service) RotateCSRFToken(ctx context.Context, sessionID string) (string
|
||||
return csrfToken, nil
|
||||
}
|
||||
|
||||
// RotateCSRFTokenForActor rotates the CSRF token across every active
|
||||
// (non-revoked) session of the given actor. Returns the count of
|
||||
// successfully rotated rows. Per-row failures are logged + skipped —
|
||||
// the function NEVER returns an error to the caller, because rotation
|
||||
// is defense-in-depth and must not block the role-mutation that
|
||||
// triggered it.
|
||||
//
|
||||
// Audit 2026-05-10 HIGH-2 closure — wires the documented "any actor-
|
||||
// role mutation rotates this actor's CSRF tokens" contract (see
|
||||
// RotateCSRFToken doc block). Pre-fix the rotate primitive existed
|
||||
// but the only call site was Service.Create (login mint).
|
||||
func (s *Service) RotateCSRFTokenForActor(ctx context.Context, actorID, actorType string) int {
|
||||
rows, err := s.sessions.ListByActor(ctx, actorID, actorType, s.tenantID)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "session: list-by-actor for csrf rotate failed",
|
||||
"actor_id", actorID, "actor_type", actorType, "err", err)
|
||||
return 0
|
||||
}
|
||||
rotated := 0
|
||||
now := s.clockNow().UTC()
|
||||
for _, sess := range rows {
|
||||
// Skip revoked / expired rows — they're not consultable anyway.
|
||||
if sess.RevokedAt != nil {
|
||||
continue
|
||||
}
|
||||
if sess.AbsoluteExpiresAt.Before(now) || sess.IdleExpiresAt.Before(now) {
|
||||
continue
|
||||
}
|
||||
if _, rerr := s.RotateCSRFToken(ctx, sess.ID); rerr != nil {
|
||||
slog.WarnContext(ctx, "session: csrf rotate per-row failed",
|
||||
"actor_id", actorID, "session_id", sess.ID, "err", rerr)
|
||||
continue
|
||||
}
|
||||
rotated++
|
||||
}
|
||||
return rotated
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Signing-key lifecycle.
|
||||
// =============================================================================
|
||||
|
||||
@@ -67,6 +67,19 @@ func (r *stubSessionRepo) Get(_ context.Context, id string) (*sessiondomain.Sess
|
||||
return &clone, nil
|
||||
}
|
||||
|
||||
func (r *stubSessionRepo) ListByActor(_ context.Context, actorID, actorType, _ string) ([]*sessiondomain.Session, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
var out []*sessiondomain.Session
|
||||
for _, row := range r.rows {
|
||||
if row.ActorID == actorID && row.ActorType == actorType {
|
||||
clone := *row
|
||||
out = append(out, &clone)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *stubSessionRepo) UpdateLastSeen(_ context.Context, id string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user