Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
231402e
Bootstrap the approval-gated GCP IAM workflow from declared authorities
briansrls Sep 28, 2026
a23a0f9
Anchor the IAM deny API model to its external authority
briansrls Sep 28, 2026
e1b9a45
Record native bootstrap qualification and deny-policy permission refusal
briansrls Sep 28, 2026
0e32267
Record fresh-token retry at the IAM deny-policy permission boundary
briansrls Sep 28, 2026
e413906
Converge temporary deny authority as a recoverable bootstrap dependency
briansrls Sep 28, 2026
2cd1900
Bind deny authority to verified organization ancestry and bounded con…
briansrls Sep 28, 2026
98b5529
Keep temporary-authority retirement independent of deny installation …
briansrls Sep 28, 2026
70009ad
Bind bootstrap authority to the verified Google subject and IAM email
briansrls Sep 28, 2026
5d05123
Record live bootstrap authority convergence and remaining pool policy…
briansrls Sep 28, 2026
07c17de
Model exclusive empty pool IAM initialization with single-publication…
briansrls Sep 29, 2026
be06816
Record exclusive pool initialization controls and credential dependency
briansrls Sep 29, 2026
da6f77d
Converge temporary exact-account token authority for bootstrap readback
briansrls Sep 29, 2026
1630edd
Allow bounded IAM propagation and retain verified probe cleanup receipt
briansrls Sep 29, 2026
85bdb4e
Converge required GCP APIs before bootstrap identity and privilege ch…
briansrls Sep 29, 2026
a22b630
Record successful IAM bootstrap and start independent approval-workfl…
briansrls Sep 29, 2026
7c513db
Record successful workflow observer authentication after bootstrap
briansrls Sep 29, 2026
96ddeaa
Record successful approval-gated GCP workflow recovery and readback
briansrls Sep 29, 2026
4358bae
Merge main into the #12565 IAM bootstrap; its new GCP operations decl…
Oct 4, 2026
c88dfdc
gcp_iam_bootstrap: enumerate closed-coproduct match arms (main's non-…
Oct 4, 2026
f407722
gcp_iam_converge witness: qualify attempt_identity_of (ambiguous with…
Oct 4, 2026
610120d
Delete the regex closure helper; the required floor already runs the …
Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 73 additions & 7 deletions dag/extdeps/cloud/gcp/iam.dag
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ fn reconcile_faithful_policy(
// There is deliberately no hand-authored permission roster in this module: a
// role->permission table written from memory is authored data wearing a citation,
// and it decays silently as Google edits the role.
type GcpPermissionProbeWire { permissions: List<String>? }

type GcpPredefinedRole {
name: String
title: String
Expand Down Expand Up @@ -493,6 +495,7 @@ fn gcp_project_lifecycle_state_from_wire(raw: String) -> GcpProjectLifecycleStat
}

type GcpProjectResourceV3 {
parent: String?
project_id: GcpProjectId from "projectId"
state: String
resource_name: String from "name"
Expand All @@ -505,13 +508,77 @@ type GcpProjectResourceV3 {
// which for a converge keyed on project_id is the ordinary already-converged answer rather than a
// failure, and is left for the caller to classify because this module does not know the caller's
// intent for an id it did not choose.
type GcpFolderResourceV3 { name: String, parent: String, state: String }

service gcp.ResourceManager {
config { endpoint: "https://cloudresourcemanager.googleapis.com", auth: Bearer, auth_input: access_token }

operation GetFolder {
requires Network
input { access_token: Secret, folder: NonEmptyStr }
output { name: String, parent: String, state: String, outcome: RestOutcome }
readonly
transport rest { method: GET, path: "/v3/\{folder\}" }
response {
200 => GcpFolderResourceV3
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation GetOrganizationIamPolicy {
requires Network
input { access_token: Secret, organization: NonEmptyStr }
output { bindings: List<GcpBinding>?, etag: String, version: Int?, outcome: RestOutcome }
readonly
transport rest { method: POST, path: "/v3/\{organization\}:getIamPolicy", body: { options: { requestedPolicyVersion: 3 } } }
response {
200 => GcpPolicy
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation SetOrganizationIamPolicy {
requires Network
input { access_token: Secret, organization: NonEmptyStr, policy: GcpPolicy }
output { bindings: List<GcpBinding>?, etag: String, version: Int?, outcome: RestOutcome }
transport rest { method: POST, path: "/v3/\{organization\}:setIamPolicy", body: { policy: policy, updateMask: "bindings,etag" } }
response {
200 => GcpPolicy
400 => GcpErrorShape
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
409 => GcpErrorShape
412 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation TestIamPermissions {
requires Network
input { access_token: Secret, project_id: GcpProjectId, permissions: List<String> }
output { permissions: List<String>?, outcome: RestOutcome }
readonly
transport rest { method: POST, path: "/v1/projects/\{project_id\}:testIamPermissions", body: { permissions: permissions } }
response {
200 => GcpPermissionProbeWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation GetProject {
requires Network
input { access_token: Secret, project_id: GcpProjectId }
output { project_id: GcpProjectId from "projectId", state: String, resource_name: String from "name", outcome: RestOutcome }
output { project_id: GcpProjectId from "projectId", state: String, resource_name: String from "name", parent: String?, outcome: RestOutcome }
readonly
transport rest { method: GET, path: "/v3/projects/\{project_id\}" }
response {
Expand Down Expand Up @@ -552,9 +619,9 @@ service gcp.ResourceManager {
operation GetIamPolicy {
requires Network
input { access_token: Secret, project_id: GcpProjectId }
output { bindings: Json, etag: String }
output { bindings: List<GcpBinding>?, etag: String, version: Int?, outcome: RestOutcome }
readonly
transport rest { method: POST, path: "/v1/projects/\{project_id\}:getIamPolicy" }
transport rest { method: POST, path: "/v1/projects/\{project_id\}:getIamPolicy", body: { options: { requestedPolicyVersion: 3 } } }
response {
200 => GcpPolicy
401 => GcpErrorShape
Expand All @@ -569,11 +636,10 @@ service gcp.ResourceManager {
input {
access_token: Secret
project_id: GcpProjectId
bindings: Json
etag: NonEmptyStr
policy: GcpPolicy
}
output { ok: Bool }
transport rest { method: POST, path: "/v1/projects/\{project_id\}:setIamPolicy" }
output { bindings: List<GcpBinding>?, etag: String, version: Int?, outcome: RestOutcome }
transport rest { method: POST, path: "/v1/projects/\{project_id\}:setIamPolicy", body: { policy: policy, updateMask: "bindings,etag" } }
response {
200 => GcpPolicy
401 => GcpErrorShape
Expand Down
123 changes: 121 additions & 2 deletions dag/extdeps/cloud/gcp/iam_admin.dag
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ module extdeps.cloud.gcp.iam_admin
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }
import extdeps.cloud.gcp.gcp { GcpProjectId, ServiceAccountEmail, WifPoolWire, WifProviderWire, WifProviderListWire }
import extdeps.cloud.gcp.iam { GcpPolicy, GcpBinding }
import extdeps.cloud.gcp.iam { GcpPolicy, GcpBinding, GcpPermissionProbeWire }
import std.types { Bool, Int, List, Map, NonEmptyStr, Secret, String }
import extdeps.transports.rest { RestOutcome }
import extdeps.transports.rest { RestOutcome }
import extdeps.ietf.http_semantics { POST }
import extdeps.ietf.http_semantics { GET, POST }
import std.resources { Network }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
Expand Down Expand Up @@ -61,13 +61,132 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
// classification that could not see it would call a pool single-provider while a second admission
// rule waits one call away. GetWorkloadIdentityPool and it are what a provisioning run reads before
// it writes anything through an existing pool (gunbc.auth.heal_publisher_provision).

// Custom roles are observed as exact permission sets, including disabled/deleted standing.
type GcpCustomRoleWire {
name: String
title: String
included_permissions: List<String>? from "includedPermissions"
stage: String?
deleted: Bool?
}

// A newly observed pool may return HTTP 200 with an entirely empty policy. Missing
// etag is a valid wire observation, not authority for an unconditional replacement.
// Only presence is consumed: pool policies with audit configuration are refused
// by this bootstrap rather than reconstructed without their logging settings.
type GcpPoolAuditConfigWire { service: String? }

type GcpPoolPolicyWire {
audit_configs: List<GcpPoolAuditConfigWire>? from "auditConfigs"
bindings: List<GcpBinding>?
etag: String?
version: Int?
}

service gcp.IamAdmin {
config {
endpoint: "https://iam.googleapis.com"
auth: Bearer
auth_input: access_token
}

operation GetProjectRole {
requires Network
input { access_token: Secret, project_id: GcpProjectId, role_id: NonEmptyStr }
output { name: String, included_permissions: List<String>? from "includedPermissions", stage: String?, deleted: Bool?, outcome: RestOutcome }
readonly
transport rest { method: GET, path: "/v1/projects/\{project_id\}/roles/\{role_id\}" }
response {
200 => GcpCustomRoleWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
429 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation CreateProjectRole {
requires Network
input { access_token: Secret, project_id: GcpProjectId, role_id: NonEmptyStr, title: NonEmptyStr, permissions: List<String> }
output { name: String, included_permissions: List<String>? from "includedPermissions", stage: String?, deleted: Bool?, outcome: RestOutcome }
transport rest { method: POST, path: "/v1/projects/\{project_id\}/roles",
body: { roleId: role_id, role: { title: title, includedPermissions: permissions, stage: "GA" } } }
response {
200 => GcpCustomRoleWire
401 => GcpErrorShape
403 => GcpErrorShape
409 => GcpErrorShape
429 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation GetWorkloadIdentityPoolIamPolicy {
requires Network
input { access_token: Secret, project_id: GcpProjectId, pool_id: NonEmptyStr }
output { audit_configs: List<GcpPoolAuditConfigWire>? from "auditConfigs", bindings: List<GcpBinding>?, etag: String?, version: Int?, outcome: RestOutcome }
readonly
transport rest { method: POST, path: "/v1/projects/\{project_id\}/locations/global/workloadIdentityPools/\{pool_id\}:getIamPolicy",
body: { options: { requestedPolicyVersion: 3 } } }
response {
200 => GcpPoolPolicyWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
429 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation SetWorkloadIdentityPoolIamPolicy {
requires Network
input { access_token: Secret, project_id: GcpProjectId, pool_id: NonEmptyStr, policy: GcpPolicy }
output { audit_configs: List<GcpPoolAuditConfigWire>? from "auditConfigs", bindings: List<GcpBinding>?, etag: String?, version: Int?, outcome: RestOutcome }
transport rest { method: POST, path: "/v1/projects/\{project_id\}/locations/global/workloadIdentityPools/\{pool_id\}:setIamPolicy", body: { policy: policy } }
response {
200 => GcpPoolPolicyWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
409 => GcpErrorShape
412 => GcpErrorShape
429 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation TestServiceAccountIamPermissions {
requires Network
input { access_token: Secret, project_id: GcpProjectId, service_account: ServiceAccountEmail, permissions: List<String> }
output { permissions: List<String>?, outcome: RestOutcome }
readonly
transport rest { method: POST, path: "/v1/projects/\{project_id\}/serviceAccounts/\{service_account\}:testIamPermissions", body: { permissions: permissions } }
response {
200 => GcpPermissionProbeWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation TestWorkloadIdentityPoolIamPermissions {
requires Network
input { access_token: Secret, project_id: GcpProjectId, pool_id: NonEmptyStr, permissions: List<String> }
output { permissions: List<String>?, outcome: RestOutcome }
readonly
transport rest { method: POST, path: "/v1/projects/\{project_id\}/locations/global/workloadIdentityPools/\{pool_id\}:testIamPermissions", body: { permissions: permissions } }
response {
200 => GcpPermissionProbeWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation CreateServiceAccount {
requires Network
input {
Expand Down
66 changes: 66 additions & 0 deletions dag/extdeps/cloud/gcp/iam_deny.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
module extdeps.cloud.gcp.iam_deny

import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }
import std.types { String, NonEmptyStr, Secret, List }
import extdeps.cloud.gcp.iam { IamCondition }
import extdeps.cloud.gcp.gcp { GcpOperation }
import extdeps.cloud.gcp.errors { GcpErrorShape }
import extdeps.transports.rest { RestOutcome }
import extdeps.ietf.http_semantics { GET, POST }
import std.resources { Network }

data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri { scheme: Https, locator: "cloud.google.com/iam/docs/reference/rest/v2/policies" }
}

// IAM v2 policies: attachment_point is one URL-encoded resource-name segment.
// https://docs.cloud.google.com/iam/docs/reference/rest/v2/policies/createPolicy
// https://docs.cloud.google.com/iam/docs/reference/rest/v2/policies
type GcpDenyRule {
denied_principals: List<String> from "deniedPrincipals"
denied_permissions: List<String> from "deniedPermissions"
exception_principals: List<String>? from "exceptionPrincipals"
exception_permissions: List<String>? from "exceptionPermissions"
denial_condition: IamCondition? from "denialCondition"
}

type GcpDenyPolicyRule { deny_rule: GcpDenyRule? from "denyRule" }

type GcpDenyPolicyWire { name: String, etag: String, rules: List<GcpDenyPolicyRule>? }

service gcp.IamDeny {
config { endpoint: "https://iam.googleapis.com", auth: Bearer, auth_input: access_token }

operation GetPolicy {
requires Network
input { access_token: Secret, attachment_point: NonEmptyStr, policy_id: NonEmptyStr }
output { name: String, etag: String, rules: List<GcpDenyPolicyRule>?, outcome: RestOutcome }
readonly
transport rest { method: GET, path: "/v2/policies/\{attachment_point\}/denypolicies/\{policy_id\}" }
response {
200 => GcpDenyPolicyWire
401 => GcpErrorShape
403 => GcpErrorShape
404 => GcpErrorShape
429 => GcpErrorShape
5xx => GcpErrorShape
}
}

operation CreatePolicy {
requires Network
input { access_token: Secret, attachment_point: NonEmptyStr, policy_id: NonEmptyStr, rules: List<GcpDenyPolicyRule> }
output { operation_name: String from "name", outcome: RestOutcome }
transport rest { method: POST, path: "/v2/policies/\{attachment_point\}/denypolicies",
query: { policyId: policy_id }, body: { rules: rules } }
response {
200 => GcpOperation
401 => GcpErrorShape
403 => GcpErrorShape
409 => GcpErrorShape
429 => GcpErrorShape
5xx => GcpErrorShape
}
}
}
4 changes: 2 additions & 2 deletions dag/extdeps/cloud/gcp/secret_manager.dag
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ type SmResolvedVersionIdentityDecodeOutcome
type GcpEmptyResponse {}

type SmTestPermissionsResponse {
permissions: List<String>
permissions: List<String>?
}

// The decoded reading of a testIamPermissions response, at one-permission grain.
Expand Down Expand Up @@ -707,7 +707,7 @@ service gcp.SecretManager {
operation TestSecretIamPermissions {
requires Network
input { access_token: Secret, project_id: GcpProjectId, secret: NonEmptyStr, permissions: List<String> }
output { permissions: List<String> }
output { permissions: List<String>?, outcome: RestOutcome }
readonly
transport rest {
method: POST,
Expand Down
38 changes: 38 additions & 0 deletions dag/extdeps/google/userinfo.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module extdeps.google.userinfo

import std.types { String, Email, Bool, Secret }
import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Uri, Https }
import extdeps.transports.rest { RestOutcome }
import extdeps.ietf.http_semantics { GET }
import extdeps.cloud.gcp.errors { GcpOAuth2ErrorShape }
import std.resources { Network }

// Flat wire projection of OIDC standard claims. Claim semantics remain in extdeps.auth.oidc.
data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority {
uri: Uri { scheme: Https, locator: "developers.google.com/identity/openid-connect/reference" }
}

type GoogleUserInfoWire {
subject: String from "sub"
email: Email?
email_verified: Bool?
}

service google.OidcUserInfo {
config { endpoint: "https://openidconnect.googleapis.com", auth: Bearer, auth_input: access_token }
operation Get {
requires Network
input { access_token: Secret }
output { subject: String from "sub", email: Email?, email_verified: Bool?, outcome: RestOutcome }
readonly
transport rest { method: GET, path: "/v1/userinfo" }
response {
200 => GoogleUserInfoWire
400 => GcpOAuth2ErrorShape
401 => GcpOAuth2ErrorShape
403 => GcpOAuth2ErrorShape
5xx => GcpOAuth2ErrorShape
}
}
}
Loading