Skip to content

Commit dd9e21f

Browse files
authored
[FEAT] Add Organization workflow permission resource (#3015)
* Add resource for Organization Actions Workflow Permissions Signed-off-by: Timo Sand <[email protected]> * Update documentation Signed-off-by: Timo Sand <[email protected]> * Upgrade to use v81 of `go-github` Signed-off-by: Timo Sand <[email protected]> * Align tests with new test structure Signed-off-by: Timo Sand <[email protected]> * Add further tests Signed-off-by: Timo Sand <[email protected]> * Refactor to use `tflog` Signed-off-by: Timo Sand <[email protected]> * Add further Trace, Debug and Info logs Signed-off-by: Timo Sand <[email protected]> * Address review comments Signed-off-by: Timo Sand <[email protected]> --------- Signed-off-by: Timo Sand <[email protected]>
1 parent b48779a commit dd9e21f

4 files changed

Lines changed: 453 additions & 0 deletions

github/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ func Provider() *schema.Provider {
212212
"github_enterprise_organization": resourceGithubEnterpriseOrganization(),
213213
"github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(),
214214
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
215+
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
215216
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
216217
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
217218
},
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
11+
"github.com/google/go-github/v81/github"
12+
"github.com/hashicorp/terraform-plugin-log/tflog"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
15+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
16+
)
17+
18+
type GithubActionsOrganizationWorkflowPermissionsErrorResponse struct {
19+
Message string `json:"message"`
20+
Errors string `json:"errors"`
21+
DocumentationURL string `json:"documentation_url"`
22+
Status string `json:"status"`
23+
}
24+
25+
func resourceGithubActionsOrganizationWorkflowPermissions() *schema.Resource {
26+
return &schema.Resource{
27+
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.",
28+
CreateContext: resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate,
29+
ReadContext: resourceGithubActionsOrganizationWorkflowPermissionsRead,
30+
UpdateContext: resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate,
31+
DeleteContext: resourceGithubActionsOrganizationWorkflowPermissionsDelete,
32+
Importer: &schema.ResourceImporter{
33+
StateContext: schema.ImportStatePassthroughContext,
34+
},
35+
36+
Schema: map[string]*schema.Schema{
37+
"organization_slug": {
38+
Type: schema.TypeString,
39+
Required: true,
40+
ForceNew: true,
41+
Description: "The slug of the Organization.",
42+
},
43+
"default_workflow_permissions": {
44+
Type: schema.TypeString,
45+
Optional: true,
46+
Default: "read",
47+
Description: "The default workflow permissions granted to the GITHUB_TOKEN when running workflows in any repository in the organization. Can be 'read' or 'write'.",
48+
ValidateFunc: validation.StringInSlice([]string{"read", "write"}, false),
49+
},
50+
"can_approve_pull_request_reviews": {
51+
Type: schema.TypeBool,
52+
Optional: true,
53+
Default: false,
54+
Description: "Whether GitHub Actions can approve pull request reviews in any repository in the organization.",
55+
},
56+
},
57+
}
58+
}
59+
60+
func handleEditWorkflowPermissionsError(ctx context.Context, err error, resp *github.Response) diag.Diagnostics {
61+
var ghErr *github.ErrorResponse
62+
if errors.As(err, &ghErr) {
63+
if ghErr.Response.StatusCode == http.StatusConflict {
64+
tflog.Info(ctx, "Detected conflict with workflow permissions", map[string]any{
65+
"status_code": ghErr.Response.StatusCode,
66+
})
67+
68+
errorResponse := &GithubActionsOrganizationWorkflowPermissionsErrorResponse{}
69+
data, readError := io.ReadAll(resp.Body)
70+
if readError == nil && data != nil {
71+
unmarshalError := json.Unmarshal(data, errorResponse)
72+
if unmarshalError != nil {
73+
tflog.Error(ctx, "Failed to unmarshal error response", map[string]any{
74+
"error": unmarshalError.Error(),
75+
})
76+
return diag.FromErr(unmarshalError)
77+
}
78+
79+
tflog.Debug(ctx, "Parsed workflow permissions conflict error", map[string]any{
80+
"message": errorResponse.Message,
81+
"errors": errorResponse.Errors,
82+
"documentation_url": errorResponse.DocumentationURL,
83+
"status": errorResponse.Status,
84+
})
85+
}
86+
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))
87+
}
88+
}
89+
90+
tflog.Trace(ctx, "Returning generic error", map[string]any{
91+
"error": err.Error(),
92+
})
93+
94+
return diag.FromErr(err)
95+
}
96+
97+
func resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
98+
tflog.Trace(ctx, "Entering Create/Update workflow permissions", map[string]any{
99+
"organization_slug": d.Get("organization_slug").(string),
100+
})
101+
102+
client := meta.(*Owner).v3client
103+
104+
organizationSlug := d.Get("organization_slug").(string)
105+
d.SetId(organizationSlug)
106+
107+
if d.IsNewResource() {
108+
tflog.Info(ctx, "Creating organization workflow permissions", map[string]any{
109+
"organization_slug": organizationSlug,
110+
})
111+
} else {
112+
tflog.Info(ctx, "Updating organization workflow permissions", map[string]any{
113+
"organization_slug": organizationSlug,
114+
})
115+
}
116+
117+
workflowPerms := github.DefaultWorkflowPermissionOrganization{}
118+
119+
if v, ok := d.GetOk("default_workflow_permissions"); ok {
120+
workflowPerms.DefaultWorkflowPermissions = github.Ptr(v.(string))
121+
}
122+
123+
if v, ok := d.GetOk("can_approve_pull_request_reviews"); ok {
124+
workflowPerms.CanApprovePullRequestReviews = github.Ptr(v.(bool))
125+
}
126+
127+
tflog.Debug(ctx, "Calling GitHub API to update workflow permissions", map[string]any{
128+
"organization_slug": organizationSlug,
129+
"default_workflow_permissions": workflowPerms.DefaultWorkflowPermissions,
130+
"can_approve_pull_request_reviews": workflowPerms.CanApprovePullRequestReviews,
131+
})
132+
_, resp, err := client.Actions.UpdateDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms)
133+
if err != nil {
134+
return handleEditWorkflowPermissionsError(ctx, err, resp)
135+
}
136+
137+
tflog.Trace(ctx, "Exiting Create/Update workflow permissions successfully", map[string]any{
138+
"organization_slug": organizationSlug,
139+
})
140+
return nil
141+
}
142+
143+
func resourceGithubActionsOrganizationWorkflowPermissionsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
144+
tflog.Trace(ctx, "Entering Read workflow permissions", map[string]any{
145+
"organization_slug": d.Id(),
146+
})
147+
148+
client := meta.(*Owner).v3client
149+
150+
organizationSlug := d.Id()
151+
tflog.Debug(ctx, "Calling GitHub API to read workflow permissions", map[string]any{
152+
"organization_slug": organizationSlug,
153+
})
154+
155+
workflowPerms, _, err := client.Actions.GetDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug)
156+
if err != nil {
157+
return diag.FromErr(err)
158+
}
159+
160+
tflog.Debug(ctx, "Retrieved workflow permissions from API", map[string]any{
161+
"organization_slug": organizationSlug,
162+
"default_workflow_permissions": workflowPerms.DefaultWorkflowPermissions,
163+
"can_approve_pull_request_reviews": workflowPerms.CanApprovePullRequestReviews,
164+
})
165+
166+
if err := d.Set("organization_slug", organizationSlug); err != nil {
167+
return diag.FromErr(err)
168+
}
169+
if err := d.Set("default_workflow_permissions", workflowPerms.DefaultWorkflowPermissions); err != nil {
170+
return diag.FromErr(err)
171+
}
172+
if err := d.Set("can_approve_pull_request_reviews", workflowPerms.CanApprovePullRequestReviews); err != nil {
173+
return diag.FromErr(err)
174+
}
175+
176+
tflog.Trace(ctx, "Exiting Read workflow permissions successfully", map[string]any{
177+
"organization_slug": organizationSlug,
178+
})
179+
180+
return nil
181+
}
182+
183+
func resourceGithubActionsOrganizationWorkflowPermissionsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
184+
tflog.Trace(ctx, "Entering Delete workflow permissions", map[string]any{
185+
"organization_slug": d.Id(),
186+
})
187+
188+
client := meta.(*Owner).v3client
189+
190+
organizationSlug := d.Id()
191+
tflog.Info(ctx, "Deleting organization workflow permissions (resetting to defaults)", map[string]any{
192+
"organization_slug": organizationSlug,
193+
})
194+
195+
// Reset to safe defaults
196+
workflowPerms := github.DefaultWorkflowPermissionOrganization{
197+
DefaultWorkflowPermissions: github.Ptr("read"),
198+
CanApprovePullRequestReviews: github.Ptr(false),
199+
}
200+
201+
tflog.Debug(ctx, "Using safe default values", map[string]any{
202+
"default_workflow_permissions": "read",
203+
"can_approve_pull_request_reviews": false,
204+
})
205+
206+
tflog.Debug(ctx, "Calling GitHub API to reset workflow permissions", map[string]any{
207+
"organization_slug": organizationSlug,
208+
"workflow_permissions": workflowPerms,
209+
})
210+
211+
_, resp, err := client.Actions.UpdateDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms)
212+
if err != nil {
213+
return handleEditWorkflowPermissionsError(ctx, err, resp)
214+
}
215+
216+
tflog.Trace(ctx, "Exiting Delete workflow permissions successfully", map[string]any{
217+
"organization_slug": organizationSlug,
218+
})
219+
220+
return nil
221+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8+
)
9+
10+
func TestAccGithubActionsOrganizationWorkflowPermissions(t *testing.T) {
11+
t.Run("creates organization workflow permissions without error", func(t *testing.T) {
12+
config := fmt.Sprintf(`
13+
resource "github_actions_organization_workflow_permissions" "test" {
14+
organization_slug = "%s"
15+
16+
default_workflow_permissions = "read"
17+
can_approve_pull_request_reviews = false
18+
}
19+
`, testAccConf.owner)
20+
21+
check := resource.ComposeTestCheckFunc(
22+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner),
23+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
24+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
25+
)
26+
27+
resource.Test(t, resource.TestCase{
28+
PreCheck: func() { skipUnlessHasOrgs(t) },
29+
ProviderFactories: providerFactories,
30+
Steps: []resource.TestStep{
31+
{
32+
Config: config,
33+
Check: check,
34+
},
35+
},
36+
})
37+
})
38+
39+
t.Run("updates organization workflow permissions without error", func(t *testing.T) {
40+
configs := map[string]string{
41+
"before": fmt.Sprintf(`
42+
resource "github_actions_organization_workflow_permissions" "test" {
43+
organization_slug = "%s"
44+
45+
default_workflow_permissions = "read"
46+
can_approve_pull_request_reviews = false
47+
}
48+
`, testAccConf.owner),
49+
50+
"after": fmt.Sprintf(`
51+
resource "github_actions_organization_workflow_permissions" "test" {
52+
organization_slug = "%s"
53+
54+
default_workflow_permissions = "write" // This change might be restricted by the Enterprise's settings
55+
can_approve_pull_request_reviews = true
56+
}
57+
`, testAccConf.owner),
58+
}
59+
60+
checks := map[string]resource.TestCheckFunc{
61+
"before": resource.ComposeTestCheckFunc(
62+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
63+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
64+
),
65+
"after": resource.ComposeTestCheckFunc(
66+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "write"),
67+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "true"),
68+
),
69+
}
70+
71+
resource.Test(t, resource.TestCase{
72+
PreCheck: func() { skipUnlessHasOrgs(t) },
73+
ProviderFactories: providerFactories,
74+
Steps: []resource.TestStep{
75+
{
76+
Config: configs["before"],
77+
Check: checks["before"],
78+
},
79+
{
80+
Config: configs["after"],
81+
Check: checks["after"],
82+
},
83+
},
84+
})
85+
})
86+
87+
t.Run("imports organization workflow permissions without error", func(t *testing.T) {
88+
config := fmt.Sprintf(`
89+
resource "github_actions_organization_workflow_permissions" "test" {
90+
organization_slug = "%s"
91+
92+
default_workflow_permissions = "read"
93+
can_approve_pull_request_reviews = false
94+
}
95+
`, testAccConf.owner)
96+
97+
check := resource.ComposeTestCheckFunc(
98+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner),
99+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
100+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
101+
)
102+
103+
resource.Test(t, resource.TestCase{
104+
PreCheck: func() { skipUnlessHasOrgs(t) },
105+
ProviderFactories: providerFactories,
106+
Steps: []resource.TestStep{
107+
{
108+
Config: config,
109+
Check: check,
110+
},
111+
{
112+
ResourceName: "github_actions_organization_workflow_permissions.test",
113+
ImportState: true,
114+
ImportStateVerify: true,
115+
},
116+
},
117+
})
118+
})
119+
120+
t.Run("deletes organization workflow permissions without error", func(t *testing.T) {
121+
config := fmt.Sprintf(`
122+
resource "github_actions_organization_workflow_permissions" "test" {
123+
organization_slug = "%s"
124+
125+
default_workflow_permissions = "write"
126+
can_approve_pull_request_reviews = true
127+
}
128+
`, testAccConf.owner)
129+
130+
resource.Test(t, resource.TestCase{
131+
PreCheck: func() { skipUnlessHasOrgs(t) },
132+
ProviderFactories: providerFactories,
133+
Steps: []resource.TestStep{
134+
{
135+
Config: config,
136+
Destroy: true,
137+
},
138+
},
139+
})
140+
})
141+
142+
t.Run("creates with minimal config using defaults", func(t *testing.T) {
143+
config := fmt.Sprintf(`
144+
resource "github_actions_organization_workflow_permissions" "test" {
145+
organization_slug = "%s"
146+
}
147+
`, testAccConf.owner)
148+
149+
check := resource.ComposeTestCheckFunc(
150+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner),
151+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
152+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
153+
)
154+
155+
resource.Test(t, resource.TestCase{
156+
PreCheck: func() { skipUnlessHasOrgs(t) },
157+
ProviderFactories: providerFactories,
158+
Steps: []resource.TestStep{
159+
{
160+
Config: config,
161+
Check: check,
162+
},
163+
},
164+
})
165+
})
166+
}

0 commit comments

Comments
 (0)