Documentation
¶
Overview ¶
Package sharedsignals implements the OpenID Shared Signals Framework and CAEP. It receives Security Event Tokens from an upstream identity provider and turns them into session revocations and durable risk signals.
Index ¶
- Constants
- Variables
- func HashSecret(raw string) string
- func NewPushSecret() (raw, hash string, err error)
- type Config
- type CreateStreamRequest
- type CreateStreamResponse
- type InboundStream
- type ListStreamsResponse
- type MemoryStore
- func (m *MemoryStore) CountEventsSince(_ context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
- func (m *MemoryStore) CreateInboundStream(_ context.Context, s *InboundStream) error
- func (m *MemoryStore) CreateSignal(_ context.Context, s *Signal) error
- func (m *MemoryStore) DeleteInboundStream(_ context.Context, streamID id.SSFStreamID) error
- func (m *MemoryStore) DeleteReceivedEvent(_ context.Context, eventID id.SSFEventID) error
- func (m *MemoryStore) GetInboundStream(_ context.Context, streamID id.SSFStreamID) (*InboundStream, error)
- func (m *MemoryStore) GetInboundStreamByPushPathHash(_ context.Context, hash string) (*InboundStream, error)
- func (m *MemoryStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
- func (m *MemoryStore) GetSubjectLink(_ context.Context, appID id.AppID, envID id.EnvironmentID, ...) (*SubjectLink, error)
- func (m *MemoryStore) InsertReceivedEvent(_ context.Context, e *ReceivedEvent) error
- func (m *MemoryStore) ListActiveSignals(_ context.Context, appID id.AppID, envID id.EnvironmentID, userID id.UserID, ...) ([]*Signal, error)
- func (m *MemoryStore) ListInboundStreams(_ context.Context, appID id.AppID) ([]*InboundStream, error)
- func (m *MemoryStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
- func (m *MemoryStore) UpdateInboundStream(_ context.Context, s *InboundStream) error
- func (m *MemoryStore) UpdateReceivedEvent(_ context.Context, e *ReceivedEvent) error
- func (m *MemoryStore) UpsertSubjectLink(_ context.Context, l *SubjectLink) error
- type MongoStore
- func (s *MongoStore) CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
- func (s *MongoStore) CreateInboundStream(ctx context.Context, in *InboundStream) error
- func (s *MongoStore) CreateSignal(ctx context.Context, sig *Signal) error
- func (s *MongoStore) DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
- func (s *MongoStore) DeleteReceivedEvent(ctx context.Context, eventID id.SSFEventID) error
- func (s *MongoStore) GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
- func (s *MongoStore) GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
- func (s *MongoStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
- func (s *MongoStore) GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID, ...) (*SubjectLink, error)
- func (s *MongoStore) InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
- func (s *MongoStore) ListActiveSignals(ctx context.Context, appID id.AppID, envID id.EnvironmentID, userID id.UserID, ...) ([]*Signal, error)
- func (s *MongoStore) ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
- func (s *MongoStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
- func (s *MongoStore) UpdateInboundStream(ctx context.Context, in *InboundStream) error
- func (s *MongoStore) UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
- func (s *MongoStore) UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
- type Plugin
- func (p *Plugin) CreateStream(ctx context.Context, appID id.AppID, envID id.EnvironmentID, ...) (*CreateStreamResponse, error)
- func (p *Plugin) DeclareSettings(m *settings.Manager) error
- func (p *Plugin) EvaluateRisk(ctx context.Context, req *riskengine.RiskRequest) (*riskengine.RiskSignal, error)
- func (p *Plugin) LinkSubject(ctx context.Context, appID id.AppID, envID id.EnvironmentID, ...) error
- func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group
- func (p *Plugin) Name() string
- func (p *Plugin) OnInit(_ context.Context, engine plugin.Engine) error
- func (p *Plugin) OnShutdown(_ context.Context) error
- func (p *Plugin) RegisterRoutes(router forge.Router) error
- func (p *Plugin) SetStore(s Store)
- func (p *Plugin) UpdateStream(ctx context.Context, streamID id.SSFStreamID, req UpdateStreamRequest) (*StreamView, error)
- type PostgresStore
- func (s *PostgresStore) CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
- func (s *PostgresStore) CreateInboundStream(ctx context.Context, in *InboundStream) error
- func (s *PostgresStore) CreateSignal(ctx context.Context, sig *Signal) error
- func (s *PostgresStore) DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
- func (s *PostgresStore) DeleteReceivedEvent(ctx context.Context, eventID id.SSFEventID) error
- func (s *PostgresStore) GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
- func (s *PostgresStore) GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
- func (s *PostgresStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
- func (s *PostgresStore) GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID, ...) (*SubjectLink, error)
- func (s *PostgresStore) InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
- func (s *PostgresStore) ListActiveSignals(ctx context.Context, appID id.AppID, envID id.EnvironmentID, userID id.UserID, ...) ([]*Signal, error)
- func (s *PostgresStore) ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
- func (s *PostgresStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
- func (s *PostgresStore) UpdateInboundStream(ctx context.Context, in *InboundStream) error
- func (s *PostgresStore) UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
- func (s *PostgresStore) UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
- type ReceivedEvent
- type ReceivedEventFilter
- type Resolution
- type Signal
- type SqliteStore
- func (s *SqliteStore) CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
- func (s *SqliteStore) CreateInboundStream(ctx context.Context, in *InboundStream) error
- func (s *SqliteStore) CreateSignal(ctx context.Context, sig *Signal) error
- func (s *SqliteStore) DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
- func (s *SqliteStore) DeleteReceivedEvent(ctx context.Context, eventID id.SSFEventID) error
- func (s *SqliteStore) GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
- func (s *SqliteStore) GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
- func (s *SqliteStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
- func (s *SqliteStore) GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID, ...) (*SubjectLink, error)
- func (s *SqliteStore) InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
- func (s *SqliteStore) ListActiveSignals(ctx context.Context, appID id.AppID, envID id.EnvironmentID, userID id.UserID, ...) ([]*Signal, error)
- func (s *SqliteStore) ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
- func (s *SqliteStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
- func (s *SqliteStore) UpdateInboundStream(ctx context.Context, in *InboundStream) error
- func (s *SqliteStore) UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
- func (s *SqliteStore) UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
- type Store
- type StreamView
- type SubjectLink
- type SubjectLinker
- type UpdateStreamRequest
Constants ¶
const ( ActionRevokeAll = "revoke_all" ActionRevokeSession = "revoke_session" ActionSignal = "signal" ActionLog = "log" ActionNone = "none" )
Actions an event can produce.
const ( StatusEnabled = "enabled" StatusPaused = "paused" StatusDisabled = "disabled" )
Stream status values, matching the SSF stream status vocabulary.
const ( EnforcementEnforce = "enforce" EnforcementObserve = "observe" )
Enforcement modes. Observe records what would have happened without doing it.
const ( OutcomePending = "pending" OutcomeApplied = "applied" OutcomeIgnored = "ignored" OutcomeUnresolved = "unresolved" OutcomeRejected = "rejected" )
Received-event outcomes.
const ( SourceSSO = "sso" SourceSocial = "social" SourceSCIM = "scim" SourceManual = "manual" )
Subject link sources.
const ( DefaultReceivedEventLimit = 50 MaxReceivedEventLimit = 500 )
Row limits for ListReceivedEvents. The audit trail grows without bound -- a busy stream writes a row per delivery -- so a caller that names no limit gets a page, not the table, and one that names an enormous limit does not get to turn a dashboard request into a full scan.
Variables ¶
var ( PostgresMigrations = migrate.NewGroup("authsome-sharedsignals", migrate.DependsOn("authsome")) SqliteMigrations = migrate.NewGroup("authsome-sharedsignals", migrate.DependsOn("authsome")) MongoMigrations = migrate.NewGroup("authsome-sharedsignals", migrate.DependsOn("authsome")) )
Migration groups, one per driver. All depend on the core authsome group because the stream tables reference authsome_apps.
var ( // SettingEnabled turns the receiver on and off without deleting streams. SettingEnabled = settings.Define("sharedsignals.enabled", true, settings.WithDisplayName("Shared Signals Enabled"), settings.WithDescription("Accept inbound CAEP events from configured streams"), settings.WithCategory("Shared Signals"), settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp), settings.WithEnforceable(), settings.WithHelpText("Turn the receiver off to stop acting on inbound events without deleting any stream"), settings.WithOrder(10), ) // SettingSignalTTLHours is how long a received signal keeps affecting risk. SettingSignalTTLHours = settings.Define("sharedsignals.signal_ttl_hours", 24, settings.WithDisplayName("Signal TTL (hours)"), settings.WithDescription("How long a received CAEP signal keeps influencing the risk score"), settings.WithCategory("Shared Signals"), settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp), settings.WithInputType(formconfig.FieldNumber), settings.WithUIValidation(formconfig.Validation{Min: intPtr(1), Max: intPtr(720)}), settings.WithHelpText("Signals decay to zero over this window. Default: 24"), settings.WithOrder(20), ) // SettingMaxActionsPerHour is the circuit breaker default for new streams. SettingMaxActionsPerHour = settings.Define("sharedsignals.max_actions_per_hour", 100, settings.WithDisplayName("Max Actions Per Hour"), settings.WithDescription("Actions one stream may take in an hour before it is paused"), settings.WithCategory("Shared Signals"), settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp), settings.WithInputType(formconfig.FieldNumber), settings.WithUIValidation(formconfig.Validation{Min: intPtr(1), Max: intPtr(100000)}), settings.WithHelpText("A transmitter that crosses this is paused and an alert is raised"), settings.WithOrder(30), ) // SettingCAEPSignalWeight is the weight the risk engine gives our signals. SettingCAEPSignalWeight = settings.Define("sharedsignals.risk_weight", 2, settings.WithDisplayName("Risk Weight"), settings.WithDescription("Weight the risk engine applies to CAEP signals"), settings.WithCategory("Shared Signals"), settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp), settings.WithInputType(formconfig.FieldNumber), settings.WithUIValidation(formconfig.Validation{Min: intPtr(1), Max: intPtr(10)}), settings.WithHelpText("A CAEP event is higher confidence than an IP heuristic. Default: 2"), settings.WithOrder(40), ) // SettingMaxRiskScore caps what a CAEP signal contributes to the score. SettingMaxRiskScore = settings.Define("sharedsignals.max_risk_score", 84, settings.WithDisplayName("Max Risk Score"), settings.WithDescription("Highest score a Shared Signals event may contribute"), settings.WithCategory("Shared Signals"), settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp), settings.WithInputType(formconfig.FieldNumber), settings.WithUIValidation(formconfig.Validation{Min: intPtr(1), Max: intPtr(100)}), settings.WithHelpText("84 keeps a confirmed compromise in the challenge band. Raise to 100 to block the sign-in outright"), settings.WithOrder(50), ) )
var ( // ErrNotFound is returned when a row does not exist. ErrNotFound = errors.New("sharedsignals: not found") // ErrDuplicateJTI is returned when a (stream_id, jti, event_type) // triple already exists. Callers treat this as a replay and answer 202, // so it must be distinguishable from every other write failure. // // The key is the triple, not just (stream_id, jti), because RFC 8417 // keys a SET's `events` object by event type URI, so a single delivery // carries at most one event of a given type but may legitimately carry // several different types under the same jti. Keying on (stream_id, // jti) alone would make the second event in a multi-event SET collide // with the row the first one just inserted, on the very first delivery. ErrDuplicateJTI = errors.New("sharedsignals: duplicate jti") )
Store errors.
Functions ¶
func HashSecret ¶
HashSecret returns the hex SHA-256 of a push secret. Only the hash is stored, so a database copy does not yield a working push endpoint.
func NewPushSecret ¶
NewPushSecret mints a URL-safe random secret and its hash. The plaintext is shown to the operator once at stream creation and never persisted.
Types ¶
type Config ¶
type Config struct {
// Audience is the aud value inbound SETs must carry. Streams inherit it
// at creation when they do not set their own.
Audience string
// SignalTTL is how long a stored signal stays active.
SignalTTL time.Duration
// MaxActionsPerHour is the circuit breaker default for new streams.
MaxActionsPerHour int
// ClockSkew is the tolerance for an iat slightly in the future.
ClockSkew time.Duration
// MaxSETAge is how far in the past an iat may be. Transmitters retry for
// a long time, so this is generous.
MaxSETAge time.Duration
// MaxBodyBytes bounds the push request body.
MaxBodyBytes int64
// MaxRiskScore caps the score this plugin hands to the risk engine.
//
// severityFor scores a session-revoked event at 100, which is honest: an
// identity provider that watched an account be taken over is as certain as
// a signal gets, and that number is what the stored signal keeps for
// forensics. But a single contributor's score becomes the whole composite,
// and riskengine blocks at 85 by default, so handing 100 straight through
// refused the sign-in outright rather than stepping it up. A user with
// correct credentials and a correct second factor could not get in.
//
// The default of 84 sits one below that threshold, so a CAEP signal
// challenges. Raise it to 100 if you would rather a confirmed compromise
// bar the door; the number is a policy statement, not a measurement.
MaxRiskScore int
// KeyRefreshInterval is how often cached JWKS entries are re-fetched in
// the background. A value of zero takes the default; a negative value
// turns the ticker off entirely, for an embedder that does not want a
// background goroutine and accepts that key freshness then depends
// wholly on inbound traffic.
KeyRefreshInterval time.Duration
}
Config configures the plugin. Every field has a sane default.
type CreateStreamRequest ¶
type CreateStreamRequest struct {
Name string `json:"name"`
Issuer string `json:"issuer"`
JWKSURI string `json:"jwks_uri"`
Audience string `json:"audience,omitempty"`
AllowedEventTypes []string `json:"allowed_event_types,omitempty"`
AllowedSubjectFormats []string `json:"allowed_subject_formats,omitempty"`
VerifiedDomains []string `json:"verified_domains,omitempty"`
ActionOverrides map[string]string `json:"action_overrides,omitempty"`
EnforcementMode string `json:"enforcement_mode,omitempty"`
MaxActionsPerHour int `json:"max_actions_per_hour,omitempty"`
}
CreateStreamRequest registers an identity provider we will accept events from.
type CreateStreamResponse ¶
type CreateStreamResponse struct {
Stream StreamView `json:"stream"`
// PushURLPath is the secret segment in the push URL.
PushURLPath string `json:"push_url_path"`
// PushToken is the bearer token the transmitter must send.
PushToken string `json:"push_token"`
// PushURL is the full URL to hand to the transmitter.
PushURL string `json:"push_url"`
}
CreateStreamResponse carries the two secrets. This is the only time either is readable; the store keeps hashes.
type InboundStream ¶
type InboundStream struct {
ID id.SSFStreamID
AppID id.AppID
EnvID id.EnvironmentID
Name string
// Issuer is the exact iss value a SET must carry.
Issuer string
// Audience is the aud value a SET must include.
Audience string
// JWKSURI is where this stream's verification keys live.
JWKSURI string
// PushPathHash is SHA-256 of the secret URL segment. The plaintext is
// shown once at creation and never stored.
PushPathHash string
// PushTokenHash is SHA-256 of the bearer token the transmitter must send.
PushTokenHash string
AllowedEventTypes []string
AllowedSubjectFormats []string
VerifiedDomains []string
ActionOverrides map[string]string
EnforcementMode string
Status string
MaxActionsPerHour int
// PendingVerifyState is the state we sent to the transmitter's
// verification endpoint and expect echoed back in a verification event.
PendingVerifyState string
LastVerifiedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
InboundStream is one identity provider we accept events from. Everything the receiver trusts about a SET comes from this row, never from the token.
type ListStreamsResponse ¶
type ListStreamsResponse struct {
Streams []StreamView `json:"streams"`
}
ListStreamsResponse is the list payload.
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-memory Store, used in tests and in standalone mode when no database is configured.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore builds an empty in-memory store.
func (*MemoryStore) CountEventsSince ¶
func (m *MemoryStore) CountEventsSince(_ context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
func (*MemoryStore) CreateInboundStream ¶
func (m *MemoryStore) CreateInboundStream(_ context.Context, s *InboundStream) error
func (*MemoryStore) CreateSignal ¶
func (m *MemoryStore) CreateSignal(_ context.Context, s *Signal) error
func (*MemoryStore) DeleteInboundStream ¶
func (m *MemoryStore) DeleteInboundStream(_ context.Context, streamID id.SSFStreamID) error
func (*MemoryStore) DeleteReceivedEvent ¶
func (m *MemoryStore) DeleteReceivedEvent(_ context.Context, eventID id.SSFEventID) error
DeleteReceivedEvent removes a row by ID. Events are keyed in storage by (stream_id, jti, event_type), not by ID, so this scans -- MemoryStore backs tests and standalone mode, never a production hot path.
func (*MemoryStore) GetInboundStream ¶
func (m *MemoryStore) GetInboundStream(_ context.Context, streamID id.SSFStreamID) (*InboundStream, error)
func (*MemoryStore) GetInboundStreamByPushPathHash ¶
func (m *MemoryStore) GetInboundStreamByPushPathHash(_ context.Context, hash string) (*InboundStream, error)
func (*MemoryStore) GetReceivedEvent ¶
func (m *MemoryStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
GetReceivedEvent loads one audit row by ID and confirms it belongs to appID. Events are keyed in storage by (stream_id, jti, event_type), not by ID, so this scans -- MemoryStore backs tests and standalone mode, never a production hot path.
func (*MemoryStore) GetSubjectLink ¶
func (m *MemoryStore) GetSubjectLink(_ context.Context, appID id.AppID, envID id.EnvironmentID, issuer, subject string) (*SubjectLink, error)
func (*MemoryStore) InsertReceivedEvent ¶
func (m *MemoryStore) InsertReceivedEvent(_ context.Context, e *ReceivedEvent) error
func (*MemoryStore) ListActiveSignals ¶
func (*MemoryStore) ListInboundStreams ¶
func (m *MemoryStore) ListInboundStreams(_ context.Context, appID id.AppID) ([]*InboundStream, error)
func (*MemoryStore) ListReceivedEvents ¶
func (m *MemoryStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
ListReceivedEvents returns one stream's audit rows newest first.
func (*MemoryStore) UpdateInboundStream ¶
func (m *MemoryStore) UpdateInboundStream(_ context.Context, s *InboundStream) error
func (*MemoryStore) UpdateReceivedEvent ¶
func (m *MemoryStore) UpdateReceivedEvent(_ context.Context, e *ReceivedEvent) error
func (*MemoryStore) UpsertSubjectLink ¶
func (m *MemoryStore) UpsertSubjectLink(_ context.Context, l *SubjectLink) error
type MongoStore ¶
type MongoStore struct {
// contains filtered or unexported fields
}
MongoStore implements Store on MongoDB.
func NewMongoStore ¶
func NewMongoStore(db *grove.DB) *MongoStore
NewMongoStore builds a MongoDB-backed store.
func (*MongoStore) CountEventsSince ¶
func (s *MongoStore) CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
func (*MongoStore) CreateInboundStream ¶
func (s *MongoStore) CreateInboundStream(ctx context.Context, in *InboundStream) error
func (*MongoStore) CreateSignal ¶
func (s *MongoStore) CreateSignal(ctx context.Context, sig *Signal) error
func (*MongoStore) DeleteInboundStream ¶
func (s *MongoStore) DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
func (*MongoStore) DeleteReceivedEvent ¶
func (s *MongoStore) DeleteReceivedEvent(ctx context.Context, eventID id.SSFEventID) error
func (*MongoStore) GetInboundStream ¶
func (s *MongoStore) GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
func (*MongoStore) GetInboundStreamByPushPathHash ¶
func (s *MongoStore) GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
func (*MongoStore) GetReceivedEvent ¶
func (s *MongoStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
GetReceivedEvent loads one audit row by ID and confirms it belongs to appID. The document carries no app_id, so the scope comes from the stream it arrived on.
func (*MongoStore) GetSubjectLink ¶
func (s *MongoStore) GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID, issuer, subject string) (*SubjectLink, error)
func (*MongoStore) InsertReceivedEvent ¶
func (s *MongoStore) InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
func (*MongoStore) ListActiveSignals ¶
func (*MongoStore) ListInboundStreams ¶
func (s *MongoStore) ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
func (*MongoStore) ListReceivedEvents ¶
func (s *MongoStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
ListReceivedEvents returns one stream's audit rows newest first. The (stream_id, received_at DESC) index from the create migration is exactly this query's access path.
func (*MongoStore) UpdateInboundStream ¶
func (s *MongoStore) UpdateInboundStream(ctx context.Context, in *InboundStream) error
func (*MongoStore) UpdateReceivedEvent ¶
func (s *MongoStore) UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
func (*MongoStore) UpsertSubjectLink ¶
func (s *MongoStore) UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin receives Shared Signals events and turns them into session revocations and durable risk signals.
func (*Plugin) CreateStream ¶
func (p *Plugin) CreateStream(ctx context.Context, appID id.AppID, envID id.EnvironmentID, req CreateStreamRequest) (*CreateStreamResponse, error)
CreateStream registers a stream and mints its two secrets.
func (*Plugin) DeclareSettings ¶
DeclareSettings implements plugin.SettingsProvider.
func (*Plugin) EvaluateRisk ¶
func (p *Plugin) EvaluateRisk(ctx context.Context, req *riskengine.RiskRequest) (*riskengine.RiskSignal, error)
EvaluateRisk replays stored signals at sign-in time. This is how an event that arrived at 02:00, when nobody was signing in, reaches the 08:00 login that cares about it. A high score crosses riskengine's medium threshold and its decision becomes "challenge", which is how step-up is expressed without inventing a second mechanism.
func (*Plugin) LinkSubject ¶
func (p *Plugin) LinkSubject(ctx context.Context, appID id.AppID, envID id.EnvironmentID, issuer, subject string, userID id.UserID, source string) error
LinkSubject records that (issuer, subject) is this user, so a later CAEP event naming that pair resolves. Calling it repeatedly is safe: the store upserts on the tuple.
func (*Plugin) MigrationGroups ¶
MigrationGroups implements plugin.MigrationProvider.
func (*Plugin) OnShutdown ¶
OnShutdown stops the background JWKS refresh and waits for any round already in flight, so the process does not exit with an outbound fetch still open against an IdP.
func (*Plugin) RegisterRoutes ¶
RegisterRoutes implements plugin.RouteProvider. The receiver endpoint lands in receiver.go and the admin CRUD in admin.go.
func (*Plugin) SetStore ¶
SetStore overrides the backing store. Tests use it; production wiring goes through OnInit.
func (*Plugin) UpdateStream ¶
func (p *Plugin) UpdateStream(ctx context.Context, streamID id.SSFStreamID, req UpdateStreamRequest) (*StreamView, error)
UpdateStream changes the mutable fields of a stream.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore implements Store on the Grove ORM with PostgreSQL.
func NewPostgresStore ¶
func NewPostgresStore(db *grove.DB) *PostgresStore
NewPostgresStore builds a PostgreSQL-backed store.
func (*PostgresStore) CountEventsSince ¶
func (s *PostgresStore) CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
func (*PostgresStore) CreateInboundStream ¶
func (s *PostgresStore) CreateInboundStream(ctx context.Context, in *InboundStream) error
func (*PostgresStore) CreateSignal ¶
func (s *PostgresStore) CreateSignal(ctx context.Context, sig *Signal) error
func (*PostgresStore) DeleteInboundStream ¶
func (s *PostgresStore) DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
func (*PostgresStore) DeleteReceivedEvent ¶
func (s *PostgresStore) DeleteReceivedEvent(ctx context.Context, eventID id.SSFEventID) error
func (*PostgresStore) GetInboundStream ¶
func (s *PostgresStore) GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
func (*PostgresStore) GetInboundStreamByPushPathHash ¶
func (s *PostgresStore) GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
func (*PostgresStore) GetReceivedEvent ¶
func (s *PostgresStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
GetReceivedEvent loads one audit row by ID and confirms it belongs to appID. The row itself carries no app_id, so the scope comes from the stream it arrived on.
func (*PostgresStore) GetSubjectLink ¶
func (s *PostgresStore) GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID, issuer, subject string) (*SubjectLink, error)
func (*PostgresStore) InsertReceivedEvent ¶
func (s *PostgresStore) InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
func (*PostgresStore) ListActiveSignals ¶
func (*PostgresStore) ListInboundStreams ¶
func (s *PostgresStore) ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
func (*PostgresStore) ListReceivedEvents ¶
func (s *PostgresStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
ListReceivedEvents returns one stream's audit rows newest first. The (stream_id, received_at DESC) index from the create migration is exactly this query's access path.
func (*PostgresStore) UpdateInboundStream ¶
func (s *PostgresStore) UpdateInboundStream(ctx context.Context, in *InboundStream) error
func (*PostgresStore) UpdateReceivedEvent ¶
func (s *PostgresStore) UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
func (*PostgresStore) UpsertSubjectLink ¶
func (s *PostgresStore) UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
type ReceivedEvent ¶
type ReceivedEvent struct {
ID id.SSFEventID
StreamID id.SSFStreamID
JTI string
EventType string
SubjectJSON string
ResolvedUserID id.UserID
Outcome string
ActionTaken string
Error string
ReceivedAt time.Time
}
ReceivedEvent is both the replay guard and the audit trail for one SET.
type ReceivedEventFilter ¶
type ReceivedEventFilter struct {
StreamID id.SSFStreamID
// Since and Until bound received_at, half-open: [Since, Until). Either
// may be zero, meaning unbounded on that side.
Since time.Time
Until time.Time
// Limit caps the rows returned. Zero or less means
// DefaultReceivedEventLimit; anything above MaxReceivedEventLimit is
// clamped down to it.
Limit int
}
ReceivedEventFilter narrows a ListReceivedEvents query. A received event carries no app_id of its own -- the stream it arrived on is what binds it to a tenant -- so StreamID is mandatory and every backend resolves the stream before it reads a single row.
type Resolution ¶
type Resolution struct {
UserID id.UserID
// SessionID is set only for a complex subject whose session member both
// parsed as a valid session ID AND was verified to belong to UserID. A
// session member that fails either check rejects the whole event rather
// than being dropped, so callers may treat a non-nil SessionID here as
// already scoped to UserID without re-checking ownership themselves.
SessionID id.SessionID
Outcome string
}
Resolution is the outcome of turning a subject identifier into one of our users. Outcome is OutcomeApplied when UserID is set, OutcomeUnresolved when the identifier was acceptable but named nobody we know, and OutcomeRejected when the identifier was not one this stream may assert.
type Signal ¶
type Signal struct {
ID id.SSFSignalID
AppID id.AppID
EnvID id.EnvironmentID
UserID id.UserID
StreamID id.SSFStreamID
EventType string
Severity int
Reason string
EventAt time.Time
ExpiresAt time.Time
CreatedAt time.Time
}
Signal is durable risk state. An event that arrives while nobody is signing in has to survive until the sign-in that cares about it.
type SqliteStore ¶
type SqliteStore struct {
// contains filtered or unexported fields
}
SqliteStore implements Store on the Grove ORM with SQLite.
func NewSqliteStore ¶
func NewSqliteStore(db *grove.DB) *SqliteStore
NewSqliteStore builds a SQLite-backed store.
func (*SqliteStore) CountEventsSince ¶
func (s *SqliteStore) CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
func (*SqliteStore) CreateInboundStream ¶
func (s *SqliteStore) CreateInboundStream(ctx context.Context, in *InboundStream) error
func (*SqliteStore) CreateSignal ¶
func (s *SqliteStore) CreateSignal(ctx context.Context, sig *Signal) error
func (*SqliteStore) DeleteInboundStream ¶
func (s *SqliteStore) DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
func (*SqliteStore) DeleteReceivedEvent ¶
func (s *SqliteStore) DeleteReceivedEvent(ctx context.Context, eventID id.SSFEventID) error
func (*SqliteStore) GetInboundStream ¶
func (s *SqliteStore) GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
func (*SqliteStore) GetInboundStreamByPushPathHash ¶
func (s *SqliteStore) GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
func (*SqliteStore) GetReceivedEvent ¶
func (s *SqliteStore) GetReceivedEvent(ctx context.Context, appID id.AppID, eventID id.SSFEventID) (*ReceivedEvent, error)
GetReceivedEvent loads one audit row by ID and confirms it belongs to appID. The row itself carries no app_id, so the scope comes from the stream it arrived on.
func (*SqliteStore) GetSubjectLink ¶
func (s *SqliteStore) GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID, issuer, subject string) (*SubjectLink, error)
func (*SqliteStore) InsertReceivedEvent ¶
func (s *SqliteStore) InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
func (*SqliteStore) ListActiveSignals ¶
func (*SqliteStore) ListInboundStreams ¶
func (s *SqliteStore) ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
func (*SqliteStore) ListReceivedEvents ¶
func (s *SqliteStore) ListReceivedEvents(ctx context.Context, appID id.AppID, f ReceivedEventFilter) ([]*ReceivedEvent, error)
ListReceivedEvents returns one stream's audit rows newest first. The (stream_id, received_at DESC) index from the create migration is exactly this query's access path.
func (*SqliteStore) UpdateInboundStream ¶
func (s *SqliteStore) UpdateInboundStream(ctx context.Context, in *InboundStream) error
func (*SqliteStore) UpdateReceivedEvent ¶
func (s *SqliteStore) UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
func (*SqliteStore) UpsertSubjectLink ¶
func (s *SqliteStore) UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
type Store ¶
type Store interface {
CreateInboundStream(ctx context.Context, s *InboundStream) error
GetInboundStream(ctx context.Context, streamID id.SSFStreamID) (*InboundStream, error)
GetInboundStreamByPushPathHash(ctx context.Context, hash string) (*InboundStream, error)
ListInboundStreams(ctx context.Context, appID id.AppID) ([]*InboundStream, error)
UpdateInboundStream(ctx context.Context, s *InboundStream) error
DeleteInboundStream(ctx context.Context, streamID id.SSFStreamID) error
UpsertSubjectLink(ctx context.Context, l *SubjectLink) error
GetSubjectLink(ctx context.Context, appID id.AppID, envID id.EnvironmentID,
issuer, subject string) (*SubjectLink, error)
// InsertReceivedEvent returns ErrDuplicateJTI when (stream_id, jti,
// event_type) already exists.
InsertReceivedEvent(ctx context.Context, e *ReceivedEvent) error
UpdateReceivedEvent(ctx context.Context, e *ReceivedEvent) error
// DeleteReceivedEvent removes one received-event row by ID. Its only
// caller is the servePush pipeline undoing InsertReceivedEvent's dedupe
// row after processing that event failed for an infrastructure reason
// rather than a policy one -- a transmitter retry must actually be
// reprocessed, not read back later as a replay of a delivery that was
// never really handled.
DeleteReceivedEvent(ctx context.Context, id id.SSFEventID) error
// GetReceivedEvent loads one audit row by ID, scoped to appID. The
// row itself has no app_id, so the scope is enforced through the
// stream it arrived on; a row belonging to another tenant answers
// ErrNotFound, never a distinguishable "forbidden".
GetReceivedEvent(ctx context.Context, appID id.AppID,
eventID id.SSFEventID) (*ReceivedEvent, error)
// ListReceivedEvents returns one stream's audit rows newest first,
// bounded by the filter's time window and row limit. A stream that
// does not belong to appID answers ErrNotFound rather than an empty
// list, so a probe cannot tell "not yours" from "yours but quiet".
ListReceivedEvents(ctx context.Context, appID id.AppID,
f ReceivedEventFilter) ([]*ReceivedEvent, error)
// CountEventsSince counts every event RECORDED for a stream since a
// cutoff, whatever outcome it reached. It backs the circuit breaker.
//
// It deliberately does not filter on action_taken. Counting only the
// events that produced an action left the entire signal-only half of
// the matrix outside the breaker: an authentic but hostile transmitter
// could push unlimited risk-level-change events at HIGH, raising every
// user's risk score, while the counter stayed at zero. The breaker
// exists to bound authentic traffic, so it has to count all of it.
CountEventsSince(ctx context.Context, streamID id.SSFStreamID, since time.Time) (int, error)
CreateSignal(ctx context.Context, s *Signal) error
ListActiveSignals(ctx context.Context, appID id.AppID, envID id.EnvironmentID,
userID id.UserID, now time.Time) ([]*Signal, error)
}
Store persists everything the receiver needs.
type StreamView ¶
type StreamView struct {
ID string `json:"id"`
Name string `json:"name"`
Issuer string `json:"issuer"`
Audience string `json:"audience"`
JWKSURI string `json:"jwks_uri"`
AllowedEventTypes []string `json:"allowed_event_types"`
AllowedSubjectFormats []string `json:"allowed_subject_formats"`
VerifiedDomains []string `json:"verified_domains"`
ActionOverrides map[string]string `json:"action_overrides"`
EnforcementMode string `json:"enforcement_mode"`
Status string `json:"status"`
MaxActionsPerHour int `json:"max_actions_per_hour"`
LastVerifiedAt *time.Time `json:"last_verified_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
StreamView is the read model. It deliberately carries no secret and no hash of one.
type SubjectLink ¶
type SubjectLink struct {
ID id.SSFLinkID
AppID id.AppID
EnvID id.EnvironmentID
Issuer string
Subject string
UserID id.UserID
Source string
CreatedAt time.Time
LastSeenAt time.Time
}
SubjectLink binds an upstream (issuer, subject) pair to an authsome user. This is what makes the iss_sub subject format resolvable.
type SubjectLinker ¶
type SubjectLinker interface {
LinkSubject(ctx context.Context, appID id.AppID, envID id.EnvironmentID,
issuer, subject string, userID id.UserID, source string) error
}
SubjectLinker is implemented by this plugin so other plugins can record the upstream identity they just authenticated without importing the concrete type. Callers reach it through engine.Plugin("sharedsignals") and a type assertion, the same way risk contributors are wired.
type UpdateStreamRequest ¶
type UpdateStreamRequest struct {
Name *string `json:"name,omitempty"`
Status *string `json:"status,omitempty"`
EnforcementMode *string `json:"enforcement_mode,omitempty"`
MaxActionsPerHour *int `json:"max_actions_per_hour,omitempty"`
JWKSURI *string `json:"jwks_uri,omitempty"`
AllowedEventTypes []string `json:"allowed_event_types,omitempty"`
AllowedSubjectFormats []string `json:"allowed_subject_formats,omitempty"`
VerifiedDomains []string `json:"verified_domains,omitempty"`
ActionOverrides map[string]string `json:"action_overrides,omitempty"`
}
UpdateStreamRequest changes the mutable parts of a stream. The two push secrets are not among them; rotate by creating a new stream.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package caep implements the OpenID CAEP event types and the RFC 9493 subject identifiers they carry.
|
Package caep implements the OpenID CAEP event types and the RFC 9493 subject identifiers they carry. |
|
Package jwksclient fetches and caches JSON Web Key Sets for inbound token verification.
|
Package jwksclient fetches and caches JSON Web Key Sets for inbound token verification. |
|
Package setjwt parses and validates RFC 8417 Security Event Tokens.
|
Package setjwt parses and validates RFC 8417 Security Event Tokens. |
|
Package ssftest provides a backend-agnostic conformance suite for the sharedsignals.Store interface.
|
Package ssftest provides a backend-agnostic conformance suite for the sharedsignals.Store interface. |