mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:11:30 +00:00
21aeed4f4e
Phase 0 closure (Path B2, post-rewrite):
addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:
// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).
Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.
Generated via:
addlicense -c "certctl LLC" -y 2026 \
-f cowork/legal/copyright-header.tpl \
-ignore '**/testdata/**' -ignore '**/*_test.go' \
cmd/ internal/
Verification:
find cmd internal -name '*.go' -not -name '*_test.go' \
-not -path '*/testdata/*' \
-exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l
Returns: 0
gofmt clean. Header additions are comments only, no compile impact.
Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
140 lines
6.3 KiB
Go
140 lines
6.3 KiB
Go
// Copyright 2026 certctl LLC. All rights reserved.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
sessiondomain "github.com/certctl-io/certctl/internal/auth/session/domain"
|
|
)
|
|
|
|
// Sentinel errors for the session repositories.
|
|
var (
|
|
// ErrSessionNotFound: Get returned no row. Phase 4 maps to 401
|
|
// (the cookie either expired or was forged with a known-good key
|
|
// id but stale session id).
|
|
ErrSessionNotFound = errors.New("session: not found")
|
|
|
|
// ErrSessionRevoked: Get found a row but RevokedAt is set. Phase 4
|
|
// maps to 401.
|
|
ErrSessionRevoked = errors.New("session: revoked")
|
|
|
|
// ErrSessionExpired: Get found a row but the absolute expiry has
|
|
// passed (Phase 4 also enforces idle expiry but that's a service-
|
|
// level check against last_seen_at, not a repository sentinel).
|
|
ErrSessionExpired = errors.New("session: expired")
|
|
|
|
// ErrSessionSigningKeyNotFound: GetActive returned no row. Phase 4
|
|
// EnsureInitialSigningKey treats this as "boot-time provisioning
|
|
// needed" and mints the first key.
|
|
ErrSessionSigningKeyNotFound = errors.New("session: signing key not found")
|
|
|
|
// ErrSessionSigningKeyInUse: Delete (full purge, not Retire) failed
|
|
// because at least one sessions row still references the key. Phase
|
|
// 4's GarbageCollect waits for sessions to expire before purging.
|
|
ErrSessionSigningKeyInUse = errors.New("session: signing key still referenced by active sessions")
|
|
)
|
|
|
|
// SessionRepository wraps the sessions table. Two cookie shapes share
|
|
// the rows: post-login sessions (1h-idle/8h-absolute) and pre-login
|
|
// sessions (10-minute TTL, IsPreLogin=true; carry OIDC state + nonce
|
|
// + PKCE verifier across the IdP redirect).
|
|
type SessionRepository interface {
|
|
// Create persists a session row. Caller MUST have called
|
|
// s.Validate(). Returns ErrAuthDuplicateName-shape on the
|
|
// extremely-unlikely id collision (the id is a 32-byte random;
|
|
// callers SHOULD generate fresh ids on the second attempt).
|
|
Create(ctx context.Context, s *sessiondomain.Session) error
|
|
|
|
// Get returns a session by id. ErrSessionNotFound on miss.
|
|
// Returns the row even if revoked / expired so the service layer
|
|
// can produce the right 401 reason code (revoked vs expired vs
|
|
// not-found are all 401 to the wire but distinguishable in audit).
|
|
Get(ctx context.Context, id string) (*sessiondomain.Session, error)
|
|
|
|
// ListByActor returns every active (non-revoked, non-expired,
|
|
// non-pre-login) session for an actor. Used by the GUI's
|
|
// /v1/auth/sessions surface so users can revoke their old laptops.
|
|
ListByActor(ctx context.Context, actorID, actorType, tenantID string) ([]*sessiondomain.Session, error)
|
|
|
|
// UpdateLastSeen sets last_seen_at = NOW() for the named session.
|
|
// Phase 4's middleware calls this on every request to keep the
|
|
// idle-expiry sliding window fresh.
|
|
UpdateLastSeen(ctx context.Context, id string) error
|
|
|
|
// UpdateCSRFTokenHash replaces the csrf_token_hash on the session
|
|
// row. Phase 4's RotateCSRFToken consumes this on login completion,
|
|
// logout, and any actor-role mutation against this actor. The hash
|
|
// is the SHA-256 hex of the operator-facing CSRF token plaintext.
|
|
UpdateCSRFTokenHash(ctx context.Context, id, csrfTokenHash string) error
|
|
|
|
// Revoke sets revoked_at = NOW() for the named session. Subsequent
|
|
// Get returns the row with RevokedAt set; Phase 4's Validate maps
|
|
// to 401.
|
|
Revoke(ctx context.Context, id string) error
|
|
|
|
// RevokeAllForActor sets revoked_at = NOW() on every active session
|
|
// for an actor. Used on role change, fired-employee scenarios, and
|
|
// the back-channel logout endpoint (Phase 5).
|
|
RevokeAllForActor(ctx context.Context, actorID, actorType, tenantID string) error
|
|
|
|
// RevokeAllExceptForActor sets revoked_at = NOW() on every active
|
|
// session for an actor EXCEPT the named exceptSessionID. Returns
|
|
// the count of rows revoked. Audit 2026-05-10 MED-3 closure —
|
|
// backs the "Sign out all other sessions" SessionsPage button.
|
|
RevokeAllExceptForActor(ctx context.Context, actorID, actorType, tenantID, exceptSessionID string) (int, error)
|
|
|
|
// GarbageCollectExpired deletes sessions whose absolute expiry
|
|
// has passed AND whose revoked_at is older than the configurable
|
|
// retention window (default 24h). Pre-login rows older than the
|
|
// 10-minute TTL are also deleted. Returns the number of rows
|
|
// deleted.
|
|
GarbageCollectExpired(ctx context.Context) (int, error)
|
|
|
|
// Delete unconditionally removes a session row. Used for the
|
|
// admin-only "purge a specific session" surface (rarely needed;
|
|
// Revoke is the normal path).
|
|
Delete(ctx context.Context, id string) error
|
|
}
|
|
|
|
// SessionSigningKeyRepository wraps the session_signing_keys table.
|
|
// Phase 4's Service.RotateSigningKey + EnsureInitialSigningKey + the
|
|
// scheduler-driven retention sweep consume this.
|
|
type SessionSigningKeyRepository interface {
|
|
// List returns every signing key in the tenant (including
|
|
// retired). Order: created_at DESC.
|
|
List(ctx context.Context, tenantID string) ([]*sessiondomain.SessionSigningKey, error)
|
|
|
|
// GetActive returns the most-recently-created non-retired key.
|
|
// ErrSessionSigningKeyNotFound when no non-retired key exists
|
|
// (Phase 4's EnsureInitialSigningKey treats this as "mint first
|
|
// key").
|
|
GetActive(ctx context.Context, tenantID string) (*sessiondomain.SessionSigningKey, error)
|
|
|
|
// Get returns one key by id (including retired keys; Phase 4's
|
|
// Validate consults this for cookies signed under retired-but-
|
|
// in-retention keys).
|
|
Get(ctx context.Context, id string) (*sessiondomain.SessionSigningKey, error)
|
|
|
|
// Add persists a new signing key. Caller MUST have called
|
|
// k.Validate() and encrypted the key_material via
|
|
// internal/crypto/encryption.go. CreatedAt defaults to NOW() if
|
|
// zero.
|
|
Add(ctx context.Context, k *sessiondomain.SessionSigningKey) error
|
|
|
|
// Retire marks an active key as retired (sets retired_at = NOW()).
|
|
// The key stays in the table for verification of cookies signed
|
|
// under it; the scheduler's retention sweep purges it after the
|
|
// configurable retention window (default 24h beyond retired_at).
|
|
Retire(ctx context.Context, id string) error
|
|
|
|
// Delete unconditionally removes a signing key row. Returns
|
|
// ErrSessionSigningKeyInUse if any sessions row still references
|
|
// the key (FK ON DELETE RESTRICT). Phase 4's GarbageCollect calls
|
|
// this only after RetentionWindow has passed AND no sessions
|
|
// reference the key.
|
|
Delete(ctx context.Context, id string) error
|
|
}
|