Skip to content

Authsec staging - #47

Merged
adityaauthnull250401 merged 30 commits into
mainfrom
authsec-staging
Aug 10, 2026
Merged

Authsec staging#47
adityaauthnull250401 merged 30 commits into
mainfrom
authsec-staging

Conversation

@adityaauthnull250401

Copy link
Copy Markdown
Contributor

Description

Changes

Testing

  • Unit tests pass (go test -short ./tests/unit/)
  • go vet ./... clean
  • Manual testing (describe below)
  • Add label run-integration to this PR to trigger integration tests in CI (requires a live DB)

Checklist

  • Code follows the project's style guidelines
  • Self-reviewed the diff
  • No secrets or credentials committed
  • Updated documentation if needed
  • Added/updated tests for new behaviour

Comment thread internal/testsupport/tokens.go Fixed
adityaauthnull250401 and others added 26 commits July 3, 2026 12:35
#1 One-transaction grant: GrantAssignment now creates the connector
assignment + broker-RS registration (approved) + connector-executor role
binding on the client's service account in a single transaction
(repo.GrantAssignmentTx). RevokeAssignment tears down the registration +
binding only when it's the client's last assignment. Replaces the manual
4-table / raw-SQL enablement with one API call.

#2 last_seen_at: service_accounts.last_seen_at is now updated on M2M token
issuance and on a successful broker action (was NULL forever).

#3 Agent activity: GET /uflow/admin/agents/:id/activity returns the agent
lens over connector_action_audit (matched by actor_client_id / subject_id /
actor_spiffe_id) for Agent 360.

Build/vet/gofmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EnsureBrokerResourceServer only wired the connector:execute oauth_scope +
its permission link inside the create branch, so a broker RS provisioned
before that wiring existed never got the link — scope resolution then failed
with "no scopes granted" forever (the manual INSERT we needed last session).
Re-assert the (idempotent) scope + link on the already-exists path too, so a
fresh service account resolves connector:execute with zero manual SQL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…undwork)

RegisterAgentClient (POST /authsec/agents) minted an XAA/A2A client with no
service_accounts row. Broker authorization binds connector:execute via
role_bindings.service_account_id, so such an agent's actor could never hold
broker scopes — a delegated XAA token acting on the broker had nothing to bind
to. Create an active service_account linked to the new client (idempotent via
uq_sa_client). Grants are unchanged (still authorization_code/refresh/
token-exchange — this is not a client_credentials M2M client); the SA link is
what lets Phase 6 delegated agents carry connector:execute.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng, refresh-lock, F8 audit

F7 (owner always): owner_email required at service-account creation (threaded
through CreateServiceAccount; implicit-create sites default to the acting
admin). Audit now records actor ALWAYS (act.client_id, else authenticating
client — fixes direct-M2M no-actor) and stamps the accountable owner into
every connector_action_audit row.

D6: reject a foreign-workspace client_id at grant time with a clear error
(resolveClientPrincipals now returns the SA workspace; GrantAssignmentTx
guards it). Cross-workspace stays the deferred A2A/XAA case.

#2a schema hardening — connector_connections: workspace_id + composite FK,
scope->binding_type, auth_type->auth_method, subject_user_id->uuid, binding/
status/auth CHECK constraints, NULL-safe partial unique indexes (a plain
UNIQUE let duplicate workspace rows through), and lifecycle cols (version,
external_account_*, refresh_expires_at, last_used_at, revoked_at).

#2b refresh-under-lock — Refresh takes a non-blocking pg advisory lock on the
connection id; a loser re-reads the rotated token instead of racing, the
winner does a version CAS and increments version. Kills the thundering-herd
refresh-token rotation race.

#2c F8 audit triad — split the overloaded outcome/http_status into
authz_outcome / broker_status / provider_status / action_outcome, so an
authorized-but-provider-failed call is no longer logged as success.

Schema change requires wipe+rebootstrap on deploy. Build/vet/gofmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…put constraints (F3)

F1 — GitHub App support. The GitHub connector can now use an org-installed
GitHub App (bot identity, no human attached) instead of an OAuth App that
impersonates whoever authorized it.
- connector_provider_apps gains app_kind ('oauth2'|'github_app') + github_app_id;
  connector_connections.auth_method CHECK now allows 'github_app'.
- internal/connectoradapters/githubapp.go: signs the App JWT (RS256, <=10m) and
  exchanges it at /app/installations/:id/access_tokens for a ~1h installation
  token, cached per installation (re-mint within 5m of expiry). No refresh flow.
- Broker runAction mints the installation token on demand for github_app
  connections and injects it (never a static Vault token).
- Endpoints: POST /providers/github/app-github (set App id + key PEM -> Vault);
  POST /:id/connections/github-app (bind installation id, no OAuth dance).
  Realizes D2 (connector-per-org-installation).

F3 — action-input constraints. An assignment can now bound WHERE an action
runs, not just WHICH action.
- connector_assignments gains input_constraints (jsonb): a per-field predicate
  {"owner":{"equals":"acme-eng"},"repo":{"glob":"release-*"}} with equals /
  one_of / glob rules (AND across fields), injection-safe glob (only *).
- Enforced as Gate 3 in runAction, AFTER input-schema validation and BEFORE the
  provider call; a violation is a policy_deny (403), fail-closed on malformed
  constraints or missing/non-string fields.
- Gate 2 now uses MatchingAssignment (action-specific row wins over all-actions)
  so the correct row's constraints apply. Threaded through the grant API/service/tx.

Schema change requires wipe+rebootstrap (or additive ADD COLUMNs) on deploy.
Build/vet/gofmt clean; Gin routes verified conflict-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctions

An authenticated end-user can now connect their OWN provider account; the
broker resolves that connection when a delegated (XAA) token carries sub=this
user. Unblocks the local per-user agent topology (T2) and per-user Google.

- StartUser: refactored Start onto a shared startConnect; the user path sets
  binding_type=user + subject_user_id (the caller's own UUID from their session,
  never supplied) on the OAuth state.
- HandleCallback branches on binding_type: user connections use a per-user Vault
  path (.../users/{subject}) and upsert keyed by (connector, subject).
- Repo: ListUserConnectionsBySubject + RevokeUserConnection (status=revoked +
  revoked_at, best-effort Vault secret delete); service wrappers.
- Endpoints (bound to caller identity via ResolveUserID):
  POST /:id/connections/user/oauth/start · GET /connections/me ·
  DELETE /:id/connections/me. Revoke -> broker fails closed on next action.

No new schema (reuses the hardened connector_connections + oauth_states).
Build/vet/gofmt clean; Gin routes verified conflict-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The broker now gates WHICH TEAM an agent may act FOR, not just which agent
calls — enforced inside the broker chain, not at the agent's own front door.

- connectors.allowed_subject_groups (uuid[]): group ids the on-behalf-of user
  must belong to; empty = no restriction.
- Gate 4 in runAction: for a delegated (XAA) call, the token subject must be a
  member of an allowed group (SubjectInAnyGroup over user_groups). A connector
  with a group policy but no human subject (M2M) is denied — the policy is
  meaningless without a subject.
- PUT /authsec/connectors/:id/subject-groups {group_ids} (connector:assign).

Closes Track A (design-review D5): #1 owner/D6, #2 schema-harden+refresh-lock+
F8, #3 GitHub App (F1), #4 input constraints (F3), #5 user consent (R4), #6
this. Additive column; rides the existing rebootstrap window. Build/vet/gofmt
clean; routes conflict-free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously a revoked client registration was a permanent dead end —
the agent got "client not authorized" with no way for the admin to
re-approve. Now:

1. authorize flow: revoked registration flips to pending_approval so
   the admin sees it in the Connections page
2. approval function: also matches 'revoked' status so the admin's
   approve action works on revoked registrations (covers both
   authorize and XAA access_request paths)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sriram695 and others added 2 commits August 5, 2026 14:51
updated the Jenkins file.

Signed-off-by: sriram <ram@authnull.com>
@adityaauthnull250401
adityaauthnull250401 merged commit f396e2d into main Aug 10, 2026
3 of 4 checks passed
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.

6 participants