Skip to content

feat(authz): publish challenges through pubsub - #5070

Open
tgmendes wants to merge 1 commit into
mainfrom
agent/publish-authz-challenges
Open

feat(authz): publish challenges through pubsub#5070
tgmendes wants to merge 1 commit into
mainfrom
agent/publish-authz-challenges

Conversation

@tgmendes

@tgmendes tgmendes commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • publish authorization challenge events through a typed Pub/Sub topic
  • consume challenge events in streams and persist them to ClickHouse
  • make ClickHouse writes idempotent across message redelivery using the challenge ID
  • update challenge queries, generated infrastructure, topology documentation, and tests

Why

Authorization challenge logging previously wrote to ClickHouse from request paths. Routing events through Pub/Sub decouples authorization handling from ClickHouse availability and gives failed writes durable retry behavior.

Impact

Authorization decisions are unchanged. Challenge logging is asynchronous, and the streams process owns ClickHouse persistence.

Validation

  • mise run gen:infra
  • mise exec -- go test ./server/internal/authz ./server/cmd/gram
  • mise exec -- go test -run=^$ ./server/internal/...
  • mise lint:server

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d89e621

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@tgmendes
tgmendes marked this pull request as ready for review August 7, 2026 17:30
@tgmendes
tgmendes requested a review from a team as a code owner August 7, 2026 17:30

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 87 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="server/internal/organizations/setup_test.go">

<violation number="1" location="server/internal/organizations/setup_test.go:138">
P3: After switching authz.NewEngine to a NoopPublisher, the organizations test package no longer uses ClickHouse at all, but TestMain still launches a ClickHouse container every run (setup_test.go line 82, ClickHouse: true). Consider removing the ClickHouse launch option so the suite no longer spins up unused infra.</violation>
</file>

<file name="server/internal/authz/challenge_writer.go">

<violation number="1" location="server/internal/authz/challenge_writer.go:90">
P2: Malformed challenge messages with a trace ID longer than 32 bytes or span ID longer than 16 bytes are treated as transient ClickHouse failures rather than poison records: `FixedString` rejects the insert, so the handler returns an error and Pub/Sub keeps redelivering the message. Validate these field sizes in `challengeRowFromMessage` before calling `InsertChallenge` so invalid records are logged and acknowledged.</violation>
</file>

<file name="server/internal/authz/challenge_writer_test.go">

<violation number="1" location="server/internal/authz/challenge_writer_test.go:46">
P2: The PR's core promise is idempotent persistence across redelivery, but no test exercises it. Add a case that calls writer.Handle twice with the same message ID (same challenge ID) and asserts authz_challenges still contains exactly one row for that id.</violation>
</file>

<file name="server/internal/access/list_challenge_buckets_test.go">

<violation number="1" location="server/internal/access/list_challenge_buckets_test.go:122">
P3: This test proves only that the buckets query returns one challenge for a duplicated challenge ID; it never exercises the feature this PR claims to add (idempotent ClickHouse writes on pubsub redelivery), because both rows are inserted straight into the table. If the SQL already dedups by challenge ID, the test passes even when the redelivery guard is missing, giving false confidence. Consider driving the rows through the real subscriber/publish path, or asserting on the underlying table state, so the test actually shields the write-side dedup.</violation>
</file>

<file name="server/cmd/gram/streams.go">

<violation number="1" location="server/cmd/gram/streams.go:329">
P2: This change reverses the resilience posture that the removed code deliberately established. Previously, a ClickHouse connect/ping failure at startup degraded gracefully: it logged the error, disabled only the ClickHouse risk_findings receiver, and let the rest of the streams process (ping, gitleaks, prompt-injection/policy scanners, webhook/Svix relay, etc.) keep running. The removed comment explicitly stated, "A ClickHouse connect/ping failure must NOT abort streams: taking the process down would also kill every other receiver."

Now a transient ClickHouse outage (or a misconfigured/unreachable host) causes the whole streams command to fail startup, so no subscriber starts at all. In addition, ClickHouse is now always required regardless of the `disable-clickhouse-risk-writes` kill switch: deployments that set that flag to avoid depending on ClickHouse still go through `newClickhouseClient` (which validates the CH config flags and pings the server) and will hard-fail at boot. Consider restoring the degrade-instead-of-abort behavior, or if the challenge writer genuinely requires ClickHouse, gate its registration separately so a ClickHouse failure disables the CH-bound receivers but not the rest of the process.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Timestamp: timestamp.UTC(),
OrganizationID: message.GetOrganizationId(),
ProjectID: message.GetProjectId(),
TraceID: message.GetTraceId(),

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Malformed challenge messages with a trace ID longer than 32 bytes or span ID longer than 16 bytes are treated as transient ClickHouse failures rather than poison records: FixedString rejects the insert, so the handler returns an error and Pub/Sub keeps redelivering the message. Validate these field sizes in challengeRowFromMessage before calling InsertChallenge so invalid records are logged and acknowledged.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/authz/challenge_writer.go, line 90:

<comment>Malformed challenge messages with a trace ID longer than 32 bytes or span ID longer than 16 bytes are treated as transient ClickHouse failures rather than poison records: `FixedString` rejects the insert, so the handler returns an error and Pub/Sub keeps redelivering the message. Validate these field sizes in `challengeRowFromMessage` before calling `InsertChallenge` so invalid records are logged and acknowledged.</comment>

<file context>
@@ -0,0 +1,115 @@
+		Timestamp:            timestamp.UTC(),
+		OrganizationID:       message.GetOrganizationId(),
+		ProjectID:            message.GetProjectId(),
+		TraceID:              message.GetTraceId(),
+		SpanID:               message.GetSpanId(),
+		RequestID:            conv.PtrEmpty(message.GetRequestId()),
</file context>
Fix with cubic

}}, row.MatchedGrants)
}

func TestChallengeCHWriterPersistsMessage(t *testing.T) {

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The PR's core promise is idempotent persistence across redelivery, but no test exercises it. Add a case that calls writer.Handle twice with the same message ID (same challenge ID) and asserts authz_challenges still contains exactly one row for that id.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/authz/challenge_writer_test.go, line 46:

<comment>The PR's core promise is idempotent persistence across redelivery, but no test exercises it. Add a case that calls writer.Handle twice with the same message ID (same challenge ID) and asserts authz_challenges still contains exactly one row for that id.</comment>

<file context>
@@ -0,0 +1,154 @@
+	}}, row.MatchedGrants)
+}
+
+func TestChallengeCHWriterPersistsMessage(t *testing.T) {
+	t.Parallel()
+
</file context>
Fix with cubic

}
chConn, shutdown, err := newClickhouseClient(ctx, logger, c)
if err != nil {
return fmt.Errorf("failed to create clickhouse client: %w", err)

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This change reverses the resilience posture that the removed code deliberately established. Previously, a ClickHouse connect/ping failure at startup degraded gracefully: it logged the error, disabled only the ClickHouse risk_findings receiver, and let the rest of the streams process (ping, gitleaks, prompt-injection/policy scanners, webhook/Svix relay, etc.) keep running. The removed comment explicitly stated, "A ClickHouse connect/ping failure must NOT abort streams: taking the process down would also kill every other receiver."

Now a transient ClickHouse outage (or a misconfigured/unreachable host) causes the whole streams command to fail startup, so no subscriber starts at all. In addition, ClickHouse is now always required regardless of the disable-clickhouse-risk-writes kill switch: deployments that set that flag to avoid depending on ClickHouse still go through newClickhouseClient (which validates the CH config flags and pings the server) and will hard-fail at boot. Consider restoring the degrade-instead-of-abort behavior, or if the challenge writer genuinely requires ClickHouse, gate its registration separately so a ClickHouse failure disables the CH-bound receivers but not the rest of the process.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/cmd/gram/streams.go, line 329:

<comment>This change reverses the resilience posture that the removed code deliberately established. Previously, a ClickHouse connect/ping failure at startup degraded gracefully: it logged the error, disabled only the ClickHouse risk_findings receiver, and let the rest of the streams process (ping, gitleaks, prompt-injection/policy scanners, webhook/Svix relay, etc.) keep running. The removed comment explicitly stated, "A ClickHouse connect/ping failure must NOT abort streams: taking the process down would also kill every other receiver."

Now a transient ClickHouse outage (or a misconfigured/unreachable host) causes the whole streams command to fail startup, so no subscriber starts at all. In addition, ClickHouse is now always required regardless of the `disable-clickhouse-risk-writes` kill switch: deployments that set that flag to avoid depending on ClickHouse still go through `newClickhouseClient` (which validates the CH config flags and pings the server) and will hard-fail at boot. Consider restoring the degrade-instead-of-abort behavior, or if the challenge writer genuinely requires ClickHouse, gate its registration separately so a ClickHouse failure disables the CH-bound receivers but not the rest of the process.</comment>

<file context>
@@ -322,26 +323,12 @@ func newStreamsCommand() *cli.Command {
-				}
+			chConn, shutdown, err := newClickhouseClient(ctx, logger, c)
+			if err != nil {
+				return fmt.Errorf("failed to create clickhouse client: %w", err)
 			}
+			shutdownFuncs = append(shutdownFuncs, shutdown)
</file context>
Fix with cubic

require.NoError(t, err)

authzEngine := authz.NewEngine(logger, conn, chConn, authztest.ChallengeLoggingAlwaysDisabled, thirdpartyworkos.NewStubClient())
authzEngine := authz.NewEngine(logger, conn, gcp.NewNoopPublisher[*authzv1.Challenge](), authztest.ChallengeLoggingAlwaysDisabled, thirdpartyworkos.NewStubClient())

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: After switching authz.NewEngine to a NoopPublisher, the organizations test package no longer uses ClickHouse at all, but TestMain still launches a ClickHouse container every run (setup_test.go line 82, ClickHouse: true). Consider removing the ClickHouse launch option so the suite no longer spins up unused infra.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/organizations/setup_test.go, line 138:

<comment>After switching authz.NewEngine to a NoopPublisher, the organizations test package no longer uses ClickHouse at all, but TestMain still launches a ClickHouse container every run (setup_test.go line 82, ClickHouse: true). Consider removing the ClickHouse launch option so the suite no longer spins up unused infra.</comment>

<file context>
@@ -133,10 +135,7 @@ func newTestOrganizationsService(t *testing.T) (context.Context, *testInstance)
-	require.NoError(t, err)
-
-	authzEngine := authz.NewEngine(logger, conn, chConn, authztest.ChallengeLoggingAlwaysDisabled, thirdpartyworkos.NewStubClient())
+	authzEngine := authz.NewEngine(logger, conn, gcp.NewNoopPublisher[*authzv1.Challenge](), authztest.ChallengeLoggingAlwaysDisabled, thirdpartyworkos.NewStubClient())
 
 	auditLogger := audit.NewLogger()
</file context>
Fix with cubic


// A subscriber can replay a message after an ambiguous acknowledgement.
// The stable event id keeps that transport replay from inflating counts.
insertCHChallenge(t, ti, authCtx.ActiveOrganizationID, challengeID, "allow", "user:u1", "org:read")

@cubic-dev-ai cubic-dev-ai Bot Aug 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test proves only that the buckets query returns one challenge for a duplicated challenge ID; it never exercises the feature this PR claims to add (idempotent ClickHouse writes on pubsub redelivery), because both rows are inserted straight into the table. If the SQL already dedups by challenge ID, the test passes even when the redelivery guard is missing, giving false confidence. Consider driving the rows through the real subscriber/publish path, or asserting on the underlying table state, so the test actually shields the write-side dedup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/internal/access/list_challenge_buckets_test.go, line 122:

<comment>This test proves only that the buckets query returns one challenge for a duplicated challenge ID; it never exercises the feature this PR claims to add (idempotent ClickHouse writes on pubsub redelivery), because both rows are inserted straight into the table. If the SQL already dedups by challenge ID, the test passes even when the redelivery guard is missing, giving false confidence. Consider driving the rows through the real subscriber/publish path, or asserting on the underlying table state, so the test actually shields the write-side dedup.</comment>

<file context>
@@ -110,6 +110,41 @@ func TestListChallengeBuckets_GroupsByDimensions(t *testing.T) {
+
+	// A subscriber can replay a message after an ambiguous acknowledgement.
+	// The stable event id keeps that transport replay from inflating counts.
+	insertCHChallenge(t, ti, authCtx.ActiveOrganizationID, challengeID, "allow", "user:u1", "org:read")
+	insertCHChallenge(t, ti, authCtx.ActiveOrganizationID, challengeID, "allow", "user:u1", "org:read")
+
</file context>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant