From 88eb3107bc7c202d5ee5c11902224088bb1e82cc Mon Sep 17 00:00:00 2001 From: Eric Pardee Date: Wed, 14 Jan 2026 08:01:14 -0800 Subject: [PATCH 1/2] fix: Prevent enterprise org taint on SAML enforcement error When creating an enterprise organization in an EMU environment, the REST API call to set description/display_name fails with a SAML enforcement error until the PAT is authorized for the new org. Previously this would taint the resource, causing Terraform to destroy and recreate the org on the next apply. This fix: - Catches SAML enforcement errors in Create and Update functions - Clears description/display_name from state on create, resets to previous values on update, so state reflects reality and next plan shows drift - Returns success instead of error to prevent tainting - Logs a warning instructing the user to authorize the PAT and re-apply Fixes: https://github.com/integrations/terraform-provider-github/issues/1914 # Conflicts: # github/resource_github_enterprise_organization.go --- ...resource_github_enterprise_organization.go | 49 +++++++++++++-- ...rce_github_enterprise_organization_test.go | 60 +++++++++++++++++++ 2 files changed, 104 insertions(+), 5 deletions(-) diff --git a/github/resource_github_enterprise_organization.go b/github/resource_github_enterprise_organization.go index fc7c36d884..83d5fc1de8 100644 --- a/github/resource_github_enterprise_organization.go +++ b/github/resource_github_enterprise_organization.go @@ -12,6 +12,19 @@ import ( "github.com/shurcooL/githubv4" ) +func isSAMLEnforcementError(err error) bool { + if err == nil { + return false + } + var ghErr *github.ErrorResponse + if errors.As(err, &ghErr) { + return ghErr.Response != nil && + ghErr.Response.StatusCode == 403 && + strings.Contains(ghErr.Message, "SAML enforcement") + } + return strings.Contains(err.Error(), "Resource protected by organization SAML enforcement") +} + func resourceGithubEnterpriseOrganization() *schema.Resource { return &schema.Resource{ Create: resourceGithubEnterpriseOrganizationCreate, @@ -128,7 +141,17 @@ func resourceGithubEnterpriseOrganizationCreate(data *schema.ResourceData, meta Name: github.Ptr(displayName), }, ) - return err + if err != nil { + if isSAMLEnforcementError(err) { + // The org was created but we can't set description/display_name until the PAT is authorized. + // Clear them from state so next plan will show drift and retry after PAT authorization. + log.Printf("[WARN] Organization %q created but could not set description/display_name due to SAML enforcement. Authorize the PAT and run apply again.", data.Get("name").(string)) + _ = data.Set("description", "") + _ = data.Set("display_name", "") + return nil + } + return err + } } return nil } @@ -301,10 +324,18 @@ func updateDescription(ctx context.Context, data *schema.ResourceData, v3 *githu ctx, orgName, &github.Organization{ - Description: github.Ptr(data.Get("description").(string)), +Description: github.Ptr(newDesc), }, ) - return err + if err != nil { + if isSAMLEnforcementError(err) { + // Reset state to old value so next plan shows drift + log.Printf("[WARN] Could not update description for %q due to SAML enforcement. Authorize the PAT and run apply again.", orgName) + _ = data.Set("description", oldDesc) + return nil + } + return err + } } return nil } @@ -318,10 +349,18 @@ func updateDisplayName(ctx context.Context, data *schema.ResourceData, v4 *githu ctx, orgName, &github.Organization{ - Name: github.Ptr(data.Get("display_name").(string)), +Name: github.Ptr(newDisplayName), }, ) - return err + if err != nil { + if isSAMLEnforcementError(err) { + // Reset state to old value so next plan shows drift + log.Printf("[WARN] Could not update display_name for %q due to SAML enforcement. Authorize the PAT and run apply again.", orgName) + _ = data.Set("display_name", oldDisplayName) + return nil + } + return err + } } return nil } diff --git a/github/resource_github_enterprise_organization_test.go b/github/resource_github_enterprise_organization_test.go index e9616bee77..4f4340b4df 100644 --- a/github/resource_github_enterprise_organization_test.go +++ b/github/resource_github_enterprise_organization_test.go @@ -1,15 +1,75 @@ package github import ( + "errors" "fmt" + "net/http" "regexp" "strings" "testing" + "github.com/google/go-github/v67/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func TestIsSAMLEnforcementError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "GitHub ErrorResponse with SAML enforcement", + err: &github.ErrorResponse{ + Response: &http.Response{StatusCode: 403}, + Message: "Resource protected by organization SAML enforcement. You must grant your Personal Access token access to this organization.", + }, + expected: true, + }, + { + name: "GitHub ErrorResponse 403 without SAML message", + err: &github.ErrorResponse{ + Response: &http.Response{StatusCode: 403}, + Message: "Forbidden", + }, + expected: false, + }, + { + name: "GitHub ErrorResponse 404", + err: &github.ErrorResponse{ + Response: &http.Response{StatusCode: 404}, + Message: "Not Found", + }, + expected: false, + }, + { + name: "plain error with SAML enforcement message", + err: errors.New("Resource protected by organization SAML enforcement"), + expected: true, + }, + { + name: "plain error without SAML message", + err: errors.New("some other error"), + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := isSAMLEnforcementError(tc.err) + if result != tc.expected { + t.Errorf("isSAMLEnforcementError(%v) = %v, want %v", tc.err, result, tc.expected) + } + }) + } +} + func TestAccGithubEnterpriseOrganization(t *testing.T) { t.Run("creates and updates an enterprise organization without error", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) From f481cf0aa7d4871ae11bacac903ceb1d3e531700 Mon Sep 17 00:00:00 2001 From: ericpardee <2423679+ericpardee@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:06 -0800 Subject: [PATCH 2/2] Update github/resource_github_enterprise_organization_test.go --- github/resource_github_enterprise_organization.go | 4 ++-- github/resource_github_enterprise_organization_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/github/resource_github_enterprise_organization.go b/github/resource_github_enterprise_organization.go index 83d5fc1de8..7a4a8e4448 100644 --- a/github/resource_github_enterprise_organization.go +++ b/github/resource_github_enterprise_organization.go @@ -324,7 +324,7 @@ func updateDescription(ctx context.Context, data *schema.ResourceData, v3 *githu ctx, orgName, &github.Organization{ -Description: github.Ptr(newDesc), + Description: github.Ptr(newDesc), }, ) if err != nil { @@ -349,7 +349,7 @@ func updateDisplayName(ctx context.Context, data *schema.ResourceData, v4 *githu ctx, orgName, &github.Organization{ -Name: github.Ptr(newDisplayName), + Name: github.Ptr(newDisplayName), }, ) if err != nil { diff --git a/github/resource_github_enterprise_organization_test.go b/github/resource_github_enterprise_organization_test.go index 4f4340b4df..fc8b380307 100644 --- a/github/resource_github_enterprise_organization_test.go +++ b/github/resource_github_enterprise_organization_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/google/go-github/v67/github" + "github.com/google/go-github/v81/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" )