diff --git a/internal/dispatch/gcf/provisioner.go b/internal/dispatch/gcf/provisioner.go index 188504dd94..098dc07852 100644 --- a/internal/dispatch/gcf/provisioner.go +++ b/internal/dispatch/gcf/provisioner.go @@ -654,24 +654,26 @@ func BuildRepoProviderID(owner, repo string) string { return raw } -// buildAttributeCondition constructs a WIF CEL condition scoped to each org's -// .fullsend repo (not org-wide) to limit which workflows can authenticate. +// buildAttributeCondition constructs a WIF CEL condition scoped to the +// organization level via repository_owner. This allows any repo in the +// org to authenticate — the mint's prevalidateOIDCToken already validates +// org membership, allowed workflow files, and workflow ref prefixes. func buildAttributeCondition(orgs []string) string { if len(orgs) == 1 { - return fmt.Sprintf("assertion.repository == '%s/.fullsend'", orgs[0]) + return fmt.Sprintf("assertion.repository_owner == '%s'", orgs[0]) } quoted := make([]string, len(orgs)) for i, org := range orgs { - quoted[i] = fmt.Sprintf("'%s/.fullsend'", org) + quoted[i] = fmt.Sprintf("'%s'", org) } - return fmt.Sprintf("assertion.repository in [%s]", strings.Join(quoted, ", ")) + return fmt.Sprintf("assertion.repository_owner in [%s]", strings.Join(quoted, ", ")) } const fullsendRepoSuffix = "/.fullsend" // parseConditionOrgs extracts GitHub org names from a WIF attribute condition. -// Supports both repo-scoped ("assertion.repository == 'org/.fullsend'") and -// legacy org-scoped ("assertion.repository_owner == 'org'") formats. +// Supports both the current org-scoped ("assertion.repository_owner == 'org'") +// and legacy repo-scoped ("assertion.repository == 'org/.fullsend'") formats. func parseConditionOrgs(condition string) []string { var orgs []string for _, part := range strings.Split(condition, "'") { diff --git a/internal/dispatch/gcf/provisioner_test.go b/internal/dispatch/gcf/provisioner_test.go index 49e960702b..9d563472bf 100644 --- a/internal/dispatch/gcf/provisioner_test.go +++ b/internal/dispatch/gcf/provisioner_test.go @@ -1242,7 +1242,7 @@ func TestProvisioner_Provision_MultiOrg_WIFCondition(t *testing.T) { _, err := p.Provision(context.Background()) require.NoError(t, err) - assert.Equal(t, "assertion.repository in ['acme/.fullsend', 'widgetco/.fullsend']", + assert.Equal(t, "assertion.repository_owner in ['acme', 'widgetco']", fake.lastWIFProviderConfig.AttributeCondition) expectedIAMAudience := "https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/fullsend-pool/providers/github-oidc" @@ -1265,7 +1265,7 @@ func TestProvisioner_Provision_SingleOrg_WIFCondition(t *testing.T) { _, err := p.Provision(context.Background()) require.NoError(t, err) - assert.Equal(t, "assertion.repository == 'acme/.fullsend'", + assert.Equal(t, "assertion.repository_owner == 'acme'", fake.lastWIFProviderConfig.AttributeCondition) expectedIAMAudience := "https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/fullsend-pool/providers/github-oidc" @@ -1355,7 +1355,7 @@ func TestProvisioner_Provision_MultiOrg_MergeDoesNotOverwriteExistingPEMs(t *tes } // WIF condition should include both orgs. - assert.Equal(t, "assertion.repository in ['existing-org/.fullsend', 'new-org/.fullsend']", + assert.Equal(t, "assertion.repository_owner in ['existing-org', 'new-org']", fake.lastWIFProviderConfig.AttributeCondition) // ROLE_APP_IDS should preserve existing-org's entries and add new-org's. @@ -1382,7 +1382,7 @@ func TestProvisionWIF_HappyPath(t *testing.T) { assert.Contains(t, fake.calls, "CreateWIFProvider") assert.Contains(t, fake.calls, "SetProjectIAMBinding") - assert.Equal(t, "assertion.repository == 'acme/.fullsend'", fake.lastWIFProviderConfig.AttributeCondition) + assert.Equal(t, "assertion.repository_owner == 'acme'", fake.lastWIFProviderConfig.AttributeCondition) } func TestProvisionWIF_MissingProjectID(t *testing.T) { @@ -1429,7 +1429,7 @@ func TestProvisionWIF_MultipleOrgs(t *testing.T) { _, err := p.ProvisionWIF(context.Background()) require.NoError(t, err) - assert.Equal(t, "assertion.repository in ['acme/.fullsend', 'beta/.fullsend']", fake.lastWIFProviderConfig.AttributeCondition) + assert.Equal(t, "assertion.repository_owner in ['acme', 'beta']", fake.lastWIFProviderConfig.AttributeCondition) require.Len(t, fake.projectIAMBindings, 2) assert.Contains(t, fake.projectIAMBindings[0].Member, "attribute.repository/acme/.fullsend") @@ -1533,7 +1533,7 @@ func TestProvisionWIF_NormalizesOrgCase(t *testing.T) { _, err := p.ProvisionWIF(context.Background()) require.NoError(t, err) - assert.Equal(t, "assertion.repository == 'acme/.fullsend'", fake.lastWIFProviderConfig.AttributeCondition) + assert.Equal(t, "assertion.repository_owner == 'acme'", fake.lastWIFProviderConfig.AttributeCondition) } func TestProvisionWIF_RepoScoped(t *testing.T) { @@ -1586,11 +1586,23 @@ func TestProvisionWIF_OrgScoped_Unchanged(t *testing.T) { require.NoError(t, err) assert.Equal(t, "github-oidc", fake.lastWIFProviderID) - assert.Equal(t, "assertion.repository == 'acme/.fullsend'", fake.lastWIFProviderConfig.AttributeCondition) + assert.Equal(t, "assertion.repository_owner == 'acme'", fake.lastWIFProviderConfig.AttributeCondition) require.Len(t, fake.projectIAMBindings, 1) assert.Contains(t, fake.projectIAMBindings[0].Member, "attribute.repository/acme/.fullsend") } +func TestBuildAttributeCondition(t *testing.T) { + t.Run("single org scopes to repository_owner", func(t *testing.T) { + got := buildAttributeCondition([]string{"myorg"}) + assert.Equal(t, "assertion.repository_owner == 'myorg'", got) + }) + + t.Run("multiple orgs uses in with repository_owner", func(t *testing.T) { + got := buildAttributeCondition([]string{"org1", "org2"}) + assert.Equal(t, "assertion.repository_owner in ['org1', 'org2']", got) + }) +} + func TestBuildRepoProviderID(t *testing.T) { tests := []struct { owner, repo string