Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ func Provider() *schema.Provider {
"github_enterprise_organization": resourceGithubEnterpriseOrganization(),
"github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(),
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
},
Expand Down
221 changes: 221 additions & 0 deletions github/resource_github_actions_organization_workflow_permissions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package github

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"github.com/google/go-github/v81/github"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

type GithubActionsOrganizationWorkflowPermissionsErrorResponse struct {
Message string `json:"message"`
Errors string `json:"errors"`
DocumentationURL string `json:"documentation_url"`
Status string `json:"status"`
}

func resourceGithubActionsOrganizationWorkflowPermissions() *schema.Resource {
return &schema.Resource{
Description: "This resource allows you to manage GitHub Actions workflow permissions for a GitHub Organization account. This controls the default permissions granted to the GITHUB_TOKEN when running workflows and whether GitHub Actions can approve pull request reviews.\n\nYou must have organization admin access to use this resource.",
CreateContext: resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate,
ReadContext: resourceGithubActionsOrganizationWorkflowPermissionsRead,
UpdateContext: resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate,
DeleteContext: resourceGithubActionsOrganizationWorkflowPermissionsDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Schema: map[string]*schema.Schema{
"organization_slug": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The slug of the Organization.",
},
"default_workflow_permissions": {
Type: schema.TypeString,
Optional: true,
Default: "read",
Description: "The default workflow permissions granted to the GITHUB_TOKEN when running workflows in any repository in the organization. Can be 'read' or 'write'.",
ValidateFunc: validation.StringInSlice([]string{"read", "write"}, false),
},
"can_approve_pull_request_reviews": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Whether GitHub Actions can approve pull request reviews in any repository in the organization.",
},
},
}
}

func handleEditWorkflowPermissionsError(ctx context.Context, err error, resp *github.Response) diag.Diagnostics {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) {
if ghErr.Response.StatusCode == http.StatusConflict {
tflog.Info(ctx, "Detected conflict with workflow permissions", map[string]any{
"status_code": ghErr.Response.StatusCode,
})

errorResponse := &GithubActionsOrganizationWorkflowPermissionsErrorResponse{}
data, readError := io.ReadAll(resp.Body)
if readError == nil && data != nil {
unmarshalError := json.Unmarshal(data, errorResponse)
if unmarshalError != nil {
tflog.Error(ctx, "Failed to unmarshal error response", map[string]any{
"error": unmarshalError.Error(),
})
return diag.FromErr(unmarshalError)
}

tflog.Debug(ctx, "Parsed workflow permissions conflict error", map[string]any{
"message": errorResponse.Message,
"errors": errorResponse.Errors,
"documentation_url": errorResponse.DocumentationURL,
"status": errorResponse.Status,
})
}
return diag.FromErr(fmt.Errorf("you are trying to modify a value restricted by the Enterprise's settings.\n Message: %s\n Errors: %s\n Documentation URL: %s\n Status: %s\nerr: %w", errorResponse.Message, errorResponse.Errors, errorResponse.DocumentationURL, errorResponse.Status, err))
}
}

tflog.Trace(ctx, "Returning generic error", map[string]any{
"error": err.Error(),
})

return diag.FromErr(err)
}

func resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
tflog.Trace(ctx, "Entering Create/Update workflow permissions", map[string]any{
"organization_slug": d.Get("organization_slug").(string),
})

client := meta.(*Owner).v3client

organizationSlug := d.Get("organization_slug").(string)
d.SetId(organizationSlug)

if d.IsNewResource() {
tflog.Info(ctx, "Creating organization workflow permissions", map[string]any{
"organization_slug": organizationSlug,
})
} else {
tflog.Info(ctx, "Updating organization workflow permissions", map[string]any{
"organization_slug": organizationSlug,
})
}

workflowPerms := github.DefaultWorkflowPermissionOrganization{}

if v, ok := d.GetOk("default_workflow_permissions"); ok {
workflowPerms.DefaultWorkflowPermissions = github.Ptr(v.(string))
}

if v, ok := d.GetOk("can_approve_pull_request_reviews"); ok {
workflowPerms.CanApprovePullRequestReviews = github.Ptr(v.(bool))
}

tflog.Debug(ctx, "Calling GitHub API to update workflow permissions", map[string]any{
"organization_slug": organizationSlug,
"default_workflow_permissions": workflowPerms.DefaultWorkflowPermissions,
"can_approve_pull_request_reviews": workflowPerms.CanApprovePullRequestReviews,
})
_, resp, err := client.Actions.UpdateDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms)
if err != nil {
return handleEditWorkflowPermissionsError(ctx, err, resp)
}

tflog.Trace(ctx, "Exiting Create/Update workflow permissions successfully", map[string]any{
"organization_slug": organizationSlug,
})
return nil
}

func resourceGithubActionsOrganizationWorkflowPermissionsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
tflog.Trace(ctx, "Entering Read workflow permissions", map[string]any{
"organization_slug": d.Id(),
})

client := meta.(*Owner).v3client

organizationSlug := d.Id()
tflog.Debug(ctx, "Calling GitHub API to read workflow permissions", map[string]any{
"organization_slug": organizationSlug,
})

workflowPerms, _, err := client.Actions.GetDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug)
if err != nil {
return diag.FromErr(err)
}

tflog.Debug(ctx, "Retrieved workflow permissions from API", map[string]any{
"organization_slug": organizationSlug,
"default_workflow_permissions": workflowPerms.DefaultWorkflowPermissions,
"can_approve_pull_request_reviews": workflowPerms.CanApprovePullRequestReviews,
})

if err := d.Set("organization_slug", organizationSlug); err != nil {
return diag.FromErr(err)
}
if err := d.Set("default_workflow_permissions", workflowPerms.DefaultWorkflowPermissions); err != nil {
return diag.FromErr(err)
}
if err := d.Set("can_approve_pull_request_reviews", workflowPerms.CanApprovePullRequestReviews); err != nil {
return diag.FromErr(err)
}

tflog.Trace(ctx, "Exiting Read workflow permissions successfully", map[string]any{
"organization_slug": organizationSlug,
})

return nil
}

func resourceGithubActionsOrganizationWorkflowPermissionsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
tflog.Trace(ctx, "Entering Delete workflow permissions", map[string]any{
"organization_slug": d.Id(),
})

client := meta.(*Owner).v3client

organizationSlug := d.Id()
tflog.Info(ctx, "Deleting organization workflow permissions (resetting to defaults)", map[string]any{
"organization_slug": organizationSlug,
})

// Reset to safe defaults
workflowPerms := github.DefaultWorkflowPermissionOrganization{
DefaultWorkflowPermissions: github.Ptr("read"),
CanApprovePullRequestReviews: github.Ptr(false),
}

tflog.Debug(ctx, "Using safe default values", map[string]any{
"default_workflow_permissions": "read",
"can_approve_pull_request_reviews": false,
})

tflog.Debug(ctx, "Calling GitHub API to reset workflow permissions", map[string]any{
"organization_slug": organizationSlug,
"workflow_permissions": workflowPerms,
})
Comment thread
deiga marked this conversation as resolved.

_, resp, err := client.Actions.UpdateDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms)
if err != nil {
return handleEditWorkflowPermissionsError(ctx, err, resp)
}

tflog.Trace(ctx, "Exiting Delete workflow permissions successfully", map[string]any{
"organization_slug": organizationSlug,
})

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package github

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccGithubActionsOrganizationWorkflowPermissions(t *testing.T) {
t.Run("creates organization workflow permissions without error", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_actions_organization_workflow_permissions" "test" {
organization_slug = "%s"

default_workflow_permissions = "read"
can_approve_pull_request_reviews = false
}
`, testAccConf.owner)

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: check,
},
},
})
})

t.Run("updates organization workflow permissions without error", func(t *testing.T) {
configs := map[string]string{
"before": fmt.Sprintf(`
resource "github_actions_organization_workflow_permissions" "test" {
organization_slug = "%s"

default_workflow_permissions = "read"
can_approve_pull_request_reviews = false
}
`, testAccConf.owner),

"after": fmt.Sprintf(`
resource "github_actions_organization_workflow_permissions" "test" {
organization_slug = "%s"

default_workflow_permissions = "write" // This change might be restricted by the Enterprise's settings
can_approve_pull_request_reviews = true
}
`, testAccConf.owner),
}

checks := map[string]resource.TestCheckFunc{
"before": resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
),
"after": resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "write"),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "true"),
),
}

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: configs["before"],
Check: checks["before"],
},
{
Config: configs["after"],
Check: checks["after"],
},
},
})
})

t.Run("imports organization workflow permissions without error", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_actions_organization_workflow_permissions" "test" {
organization_slug = "%s"

default_workflow_permissions = "read"
can_approve_pull_request_reviews = false
}
`, testAccConf.owner)

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: check,
},
{
ResourceName: "github_actions_organization_workflow_permissions.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
})

t.Run("deletes organization workflow permissions without error", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_actions_organization_workflow_permissions" "test" {
organization_slug = "%s"

default_workflow_permissions = "write"
can_approve_pull_request_reviews = true
}
`, testAccConf.owner)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Destroy: true,
},
},
})
})

t.Run("creates with minimal config using defaults", func(t *testing.T) {
config := fmt.Sprintf(`
resource "github_actions_organization_workflow_permissions" "test" {
organization_slug = "%s"
}
`, testAccConf.owner)

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: check,
},
},
})
})
}
Loading