diff --git a/examples/oidc_custom_property/main.tf b/examples/oidc_custom_property/main.tf new file mode 100644 index 0000000000..06ce697851 --- /dev/null +++ b/examples/oidc_custom_property/main.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + } + } +} + +provider "github" { + owner = var.github_owner +} + +variable "github_owner" { + description = "The GitHub organization to manage" + type = string +} + +# Step 1: Define an org-level custom property +resource "github_organization_custom_properties" "environment" { + property_name = "environment" + value_type = "single_select" + required = false + allowed_values = ["production", "staging", "development"] +} + +# Step 2: Include the custom property in OIDC tokens +resource "github_actions_organization_oidc_custom_property_inclusion" "environment" { + custom_property_name = "environment" + depends_on = [github_organization_custom_properties.environment] +} + +# Step 3: Read back the inclusions via data source +data "github_actions_organization_oidc_custom_property_inclusions" "current" { + depends_on = [github_actions_organization_oidc_custom_property_inclusion.environment] +} + +output "included_properties" { + value = data.github_actions_organization_oidc_custom_property_inclusions.current.custom_property_names +} diff --git a/github/data_source_github_actions_organization_oidc_custom_property_inclusions.go b/github/data_source_github_actions_organization_oidc_custom_property_inclusions.go new file mode 100644 index 0000000000..2b8659c0d2 --- /dev/null +++ b/github/data_source_github_actions_organization_oidc_custom_property_inclusions.go @@ -0,0 +1,52 @@ +package github + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubActionsOrganizationOIDCCustomPropertyInclusions() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceGithubActionsOrganizationOIDCCustomPropertyInclusionsRead, + + Schema: map[string]*schema.Schema{ + "custom_property_names": { + Type: schema.TypeList, + Computed: true, + Description: "A list of custom property names included in the OIDC token.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceGithubActionsOrganizationOIDCCustomPropertyInclusionsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + + err := checkOrganization(meta) + if err != nil { + return diag.FromErr(err) + } + + inclusions, err := listOrgOIDCCustomPropertyInclusions(ctx, client, orgName) + if err != nil { + return diag.FromErr(err) + } + + propertyNames := make([]string, len(inclusions)) + for i, inclusion := range inclusions { + propertyNames[i] = inclusion.PropertyName + } + + d.SetId(orgName) + if err := d.Set("custom_property_names", propertyNames); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/provider.go b/github/provider.go index 2d5d6dc33a..a3f3e23d25 100644 --- a/github/provider.go +++ b/github/provider.go @@ -137,6 +137,7 @@ func Provider() *schema.Provider { "github_actions_environment_secret": resourceGithubActionsEnvironmentSecret(), "github_actions_environment_variable": resourceGithubActionsEnvironmentVariable(), "github_actions_organization_oidc_subject_claim_customization_template": resourceGithubActionsOrganizationOIDCSubjectClaimCustomizationTemplate(), + "github_actions_organization_oidc_custom_property_inclusion": resourceGithubActionsOrganizationOIDCCustomPropertyInclusion(), "github_actions_organization_permissions": resourceGithubActionsOrganizationPermissions(), "github_actions_organization_secret": resourceGithubActionsOrganizationSecret(), "github_actions_organization_secret_repositories": resourceGithubActionsOrganizationSecretRepositories(), @@ -225,6 +226,7 @@ func Provider() *schema.Provider { "github_actions_environment_secrets": dataSourceGithubActionsEnvironmentSecrets(), "github_actions_environment_variables": dataSourceGithubActionsEnvironmentVariables(), "github_actions_organization_oidc_subject_claim_customization_template": dataSourceGithubActionsOrganizationOIDCSubjectClaimCustomizationTemplate(), + "github_actions_organization_oidc_custom_property_inclusions": dataSourceGithubActionsOrganizationOIDCCustomPropertyInclusions(), "github_actions_organization_public_key": dataSourceGithubActionsOrganizationPublicKey(), "github_actions_organization_registration_token": dataSourceGithubActionsOrganizationRegistrationToken(), "github_actions_organization_secrets": dataSourceGithubActionsOrganizationSecrets(), diff --git a/github/resource_github_actions_organization_oidc_custom_property_inclusion.go b/github/resource_github_actions_organization_oidc_custom_property_inclusion.go new file mode 100644 index 0000000000..b011332e0c --- /dev/null +++ b/github/resource_github_actions_organization_oidc_custom_property_inclusion.go @@ -0,0 +1,155 @@ +package github + +import ( + "context" + "fmt" + "log" + + "github.com/google/go-github/v84/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const oidcCustomPropertyAPIVersion = "2026-03-10" + +func resourceGithubActionsOrganizationOIDCCustomPropertyInclusion() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubActionsOrganizationOIDCCustomPropertyInclusionCreate, + Read: resourceGithubActionsOrganizationOIDCCustomPropertyInclusionRead, + Delete: resourceGithubActionsOrganizationOIDCCustomPropertyInclusionDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "custom_property_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the custom property to include in the OIDC token.", + }, + }, + } +} + +func resourceGithubActionsOrganizationOIDCCustomPropertyInclusionCreate(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + ctx := context.Background() + + err := checkOrganization(meta) + if err != nil { + return err + } + + customPropertyName := d.Get("custom_property_name").(string) + + body := map[string]string{ + "custom_property_name": customPropertyName, + } + + req, err := client.NewRequest("POST", fmt.Sprintf("orgs/%s/actions/oidc/customization/properties/repo", orgName), body) + if err != nil { + return fmt.Errorf("error creating request to add OIDC custom property inclusion: %w", err) + } + req.Header.Set("X-GitHub-Api-Version", oidcCustomPropertyAPIVersion) + + _, err = client.Do(ctx, req, nil) + if err != nil { + return fmt.Errorf("error adding OIDC custom property inclusion %q for organization %q: %w", customPropertyName, orgName, err) + } + + log.Printf("[DEBUG] Successfully added OIDC custom property inclusion %q for organization %q", customPropertyName, orgName) + + d.SetId(buildTwoPartID(orgName, customPropertyName)) + + return resourceGithubActionsOrganizationOIDCCustomPropertyInclusionRead(d, meta) +} + +func resourceGithubActionsOrganizationOIDCCustomPropertyInclusionRead(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + ctx := context.Background() + + orgName, customPropertyName, err := parseTwoPartID(d.Id(), "organization", "custom_property_name") + if err != nil { + return err + } + + err = checkOrganization(meta) + if err != nil { + return err + } + + inclusions, err := listOrgOIDCCustomPropertyInclusions(ctx, client, orgName) + if err != nil { + return fmt.Errorf("error reading OIDC custom property inclusions for organization %q: %w", orgName, err) + } + + found := false + for _, inclusion := range inclusions { + if inclusion.PropertyName == customPropertyName { + found = true + break + } + } + + if !found { + d.SetId("") + return nil + } + + if err := d.Set("custom_property_name", customPropertyName); err != nil { + return err + } + + return nil +} + +func resourceGithubActionsOrganizationOIDCCustomPropertyInclusionDelete(d *schema.ResourceData, meta any) error { + client := meta.(*Owner).v3client + ctx := context.Background() + + orgName, customPropertyName, err := parseTwoPartID(d.Id(), "organization", "custom_property_name") + if err != nil { + return err + } + + err = checkOrganization(meta) + if err != nil { + return err + } + + req, err := client.NewRequest("DELETE", fmt.Sprintf("orgs/%s/actions/oidc/customization/properties/repo/%s", orgName, customPropertyName), nil) + if err != nil { + return fmt.Errorf("error creating request to delete OIDC custom property inclusion: %w", err) + } + req.Header.Set("X-GitHub-Api-Version", oidcCustomPropertyAPIVersion) + + _, err = client.Do(ctx, req, nil) + if err != nil { + return fmt.Errorf("error deleting OIDC custom property inclusion %q for organization %q: %w", customPropertyName, orgName, err) + } + + return nil +} + +// OIDCCustomPropertyInclusion represents a custom property included in OIDC tokens. +type OIDCCustomPropertyInclusion struct { + PropertyName string `json:"custom_property_name"` + InclusionSource string `json:"inclusion_source,omitempty"` +} + +// listOrgOIDCCustomPropertyInclusions lists all custom properties included in OIDC tokens for an organization. +func listOrgOIDCCustomPropertyInclusions(ctx context.Context, client *github.Client, orgName string) ([]*OIDCCustomPropertyInclusion, error) { + req, err := client.NewRequest("GET", fmt.Sprintf("orgs/%s/actions/oidc/customization/properties/repo", orgName), nil) + if err != nil { + return nil, err + } + req.Header.Set("X-GitHub-Api-Version", oidcCustomPropertyAPIVersion) + + var inclusions []*OIDCCustomPropertyInclusion + _, err = client.Do(ctx, req, &inclusions) + if err != nil { + return nil, err + } + + return inclusions, nil +} diff --git a/github/resource_github_actions_organization_oidc_custom_property_inclusion_test.go b/github/resource_github_actions_organization_oidc_custom_property_inclusion_test.go new file mode 100644 index 0000000000..b053880e0e --- /dev/null +++ b/github/resource_github_actions_organization_oidc_custom_property_inclusion_test.go @@ -0,0 +1,160 @@ +package github + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGithubActionsOrganizationOIDCCustomPropertyInclusion(t *testing.T) { + t.Run("creates and deletes an OIDC custom property inclusion without error", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "test" { + property_name = "tf-acc-test-oidc-env" + value_type = "single_select" + required = false + allowed_values = ["production", "staging"] + } + + resource "github_actions_organization_oidc_custom_property_inclusion" "test" { + custom_property_name = github_organization_custom_properties.test.property_name + }` + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_oidc_custom_property_inclusion.test", + "custom_property_name", "tf-acc-test-oidc-env", + ), + ) + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) + + t.Run("imports an OIDC custom property inclusion without error", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "test" { + property_name = "tf-acc-test-oidc-import" + value_type = "single_select" + required = false + allowed_values = ["production", "staging"] + } + + resource "github_actions_organization_oidc_custom_property_inclusion" "test" { + custom_property_name = github_organization_custom_properties.test.property_name + }` + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_oidc_custom_property_inclusion.test", + "custom_property_name", "tf-acc-test-oidc-import", + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + ResourceName: "github_actions_organization_oidc_custom_property_inclusion.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("manages multiple OIDC custom property inclusions", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "env" { + property_name = "tf-acc-test-oidc-env2" + value_type = "single_select" + required = false + allowed_values = ["production", "staging"] + } + + resource "github_organization_custom_properties" "team" { + property_name = "tf-acc-test-oidc-team" + value_type = "string" + required = false + } + + resource "github_actions_organization_oidc_custom_property_inclusion" "env" { + custom_property_name = github_organization_custom_properties.env.property_name + } + + resource "github_actions_organization_oidc_custom_property_inclusion" "team" { + custom_property_name = github_organization_custom_properties.team.property_name + }` + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_actions_organization_oidc_custom_property_inclusion.env", + "custom_property_name", "tf-acc-test-oidc-env2", + ), + resource.TestCheckResourceAttr( + "github_actions_organization_oidc_custom_property_inclusion.team", + "custom_property_name", "tf-acc-test-oidc-team", + ), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) +} + +func TestAccGithubActionsOrganizationOIDCCustomPropertyInclusionsDataSource(t *testing.T) { + t.Run("reads OIDC custom property inclusions without error", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "test" { + property_name = "tf-acc-test-oidc-ds" + value_type = "single_select" + required = false + allowed_values = ["production", "staging"] + } + + resource "github_actions_organization_oidc_custom_property_inclusion" "test" { + custom_property_name = github_organization_custom_properties.test.property_name + } + + data "github_actions_organization_oidc_custom_property_inclusions" "test" { + depends_on = [github_actions_organization_oidc_custom_property_inclusion.test] + }` + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "data.github_actions_organization_oidc_custom_property_inclusions.test", + "custom_property_names.#", + ), + ) + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + }) +} diff --git a/website/docs/d/actions_organization_oidc_custom_property_inclusions.html.markdown b/website/docs/d/actions_organization_oidc_custom_property_inclusions.html.markdown new file mode 100644 index 0000000000..fb0c0adae2 --- /dev/null +++ b/website/docs/d/actions_organization_oidc_custom_property_inclusions.html.markdown @@ -0,0 +1,29 @@ +--- +layout: "github" +page_title: "GitHub: github_actions_organization_oidc_custom_property_inclusions" +description: |- + Lists the repository custom properties included in OIDC tokens for a GitHub organization +--- + +# github_actions_organization_oidc_custom_property_inclusions + +Use this data source to retrieve the list of repository custom properties that are included in the OIDC token for +repository actions in a GitHub organization. + +## Example Usage + +```hcl +data "github_actions_organization_oidc_custom_property_inclusions" "example" {} + +output "included_properties" { + value = data.github_actions_organization_oidc_custom_property_inclusions.example.custom_property_names +} +``` + +## Argument Reference + +This data source has no required arguments. + +## Attributes Reference + +* `custom_property_names` - A list of custom property names that are included in the OIDC token. diff --git a/website/docs/r/actions_organization_oidc_custom_property_inclusion.html.markdown b/website/docs/r/actions_organization_oidc_custom_property_inclusion.html.markdown new file mode 100644 index 0000000000..93e7209cfc --- /dev/null +++ b/website/docs/r/actions_organization_oidc_custom_property_inclusion.html.markdown @@ -0,0 +1,91 @@ +--- +layout: "github" +page_title: "GitHub: github_actions_organization_oidc_custom_property_inclusion" +description: |- + Manages a custom property inclusion in OIDC tokens for a GitHub organization +--- + +# github_actions_organization_oidc_custom_property_inclusion + +This resource allows you to add a repository custom property to be included in the OIDC token for repository actions +in a GitHub organization. + +When a custom property is included, its value will be available as a claim in the OIDC token issued to GitHub Actions +workflows. This enables cloud providers to make authorization decisions based on repository custom properties. + +More information on integrating GitHub with cloud providers using OpenID Connect and a list of available claims is +available in the [Actions documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect). + +~> **Note:** This resource requires the organization to have custom properties already defined. The custom property +referenced by `custom_property_name` must exist as an organization-level custom property. + +## Example Usage + +### Single custom property inclusion + +```hcl +resource "github_actions_organization_oidc_custom_property_inclusion" "environment" { + custom_property_name = "environment" +} +``` + +### Multiple custom property inclusions with repository custom properties + +```hcl +resource "github_organization_custom_properties" "props" { + property { + property_name = "environment" + value_type = "single_select" + required = true + allowed_values = ["production", "staging", "development"] + } + + property { + property_name = "team" + value_type = "string" + } +} + +# Include custom properties in OIDC tokens +resource "github_actions_organization_oidc_custom_property_inclusion" "environment" { + custom_property_name = "environment" +} + +resource "github_actions_organization_oidc_custom_property_inclusion" "team" { + custom_property_name = "team" +} + +# Set custom property values on a repository +resource "github_repository" "example" { + name = "example-repository" +} + +resource "github_repository_custom_property" "env" { + repository = github_repository.example.name + property_name = "environment" + property_type = "single_select" + property_value = ["production"] +} + +# Configure OIDC subject claim to include the custom property +resource "github_actions_repository_oidc_subject_claim_customization_template" "example" { + repository = github_repository.example.name + use_default = false + include_claim_keys = ["repo", "context", "repository_custom_property_environment"] +} +``` + +## Argument Reference + +The following arguments are supported: + +- `custom_property_name` - (Required) The name of the custom property to include in the OIDC token. This must match + an existing organization-level custom property. + +## Import + +This resource can be imported using the organization name and custom property name separated by a colon (`:`). + +``` +$ terraform import github_actions_organization_oidc_custom_property_inclusion.environment organization-name:environment +```