Skip to content

Commit c4572b6

Browse files
committed
Add resource for Organization Actions Workflow Permissions
Signed-off-by: Timo Sand <timo.sand@f-secure.com>
1 parent c2a1941 commit c4572b6

3 files changed

Lines changed: 284 additions & 0 deletions

github/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ func Provider() *schema.Provider {
210210
"github_enterprise_organization": resourceGithubEnterpriseOrganization(),
211211
"github_enterprise_actions_runner_group": resourceGithubActionsEnterpriseRunnerGroup(),
212212
"github_enterprise_actions_workflow_permissions": resourceGithubEnterpriseActionsWorkflowPermissions(),
213+
"github_actions_organization_workflow_permissions": resourceGithubActionsOrganizationWorkflowPermissions(),
213214
"github_enterprise_security_analysis_settings": resourceGithubEnterpriseSecurityAnalysisSettings(),
214215
"github_workflow_repository_permissions": resourceGithubWorkflowRepositoryPermissions(),
215216
},
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"log"
10+
"net/http"
11+
12+
"github.com/google/go-github/v67/github"
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: "GitHub Actions Organization Workflow Permissions management.",
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(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+
errorResponse := &GithubActionsOrganizationWorkflowPermissionsErrorResponse{}
65+
data, readError := io.ReadAll(resp.Body)
66+
if readError == nil && data != nil {
67+
unmarshalError := json.Unmarshal(data, errorResponse)
68+
if unmarshalError != nil {
69+
return diag.FromErr(unmarshalError)
70+
}
71+
}
72+
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))
73+
}
74+
}
75+
return diag.FromErr(err)
76+
}
77+
78+
func resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
79+
client := meta.(*Owner).v3client
80+
81+
organizationSlug := d.Get("organization_slug").(string)
82+
d.SetId(organizationSlug)
83+
84+
workflowPerms := github.DefaultWorkflowPermissionOrganization{}
85+
86+
if v, ok := d.GetOk("default_workflow_permissions"); ok {
87+
workflowPerms.DefaultWorkflowPermissions = github.String(v.(string))
88+
}
89+
90+
if v, ok := d.GetOk("can_approve_pull_request_reviews"); ok {
91+
workflowPerms.CanApprovePullRequestReviews = github.Bool(v.(bool))
92+
}
93+
94+
log.Printf("[DEBUG] Updating workflow permissions for Organization: %s", organizationSlug)
95+
_, resp, err := client.Actions.EditDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms)
96+
if err != nil {
97+
return handleEditWorkflowPermissionsError(err, resp)
98+
}
99+
100+
// Calling read is necessary as the Update API returns 204 with Empty Body on success
101+
return resourceGithubActionsOrganizationWorkflowPermissionsRead(ctx, d, meta)
102+
}
103+
104+
func resourceGithubActionsOrganizationWorkflowPermissionsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
105+
client := meta.(*Owner).v3client
106+
107+
organizationSlug := d.Id()
108+
log.Printf("[DEBUG] Reading workflow permissions for Organization: %s", organizationSlug)
109+
110+
workflowPerms, _, err := client.Actions.GetDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug)
111+
if err != nil {
112+
return diag.FromErr(err)
113+
}
114+
115+
if err := d.Set("organization_slug", organizationSlug); err != nil {
116+
return diag.FromErr(err)
117+
}
118+
if err := d.Set("default_workflow_permissions", workflowPerms.DefaultWorkflowPermissions); err != nil {
119+
return diag.FromErr(err)
120+
}
121+
if err := d.Set("can_approve_pull_request_reviews", workflowPerms.CanApprovePullRequestReviews); err != nil {
122+
return diag.FromErr(err)
123+
}
124+
125+
return nil
126+
}
127+
128+
func resourceGithubActionsOrganizationWorkflowPermissionsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
129+
client := meta.(*Owner).v3client
130+
131+
organizationSlug := d.Id()
132+
log.Printf("[DEBUG] Resetting workflow permissions to defaults for Organization: %s", organizationSlug)
133+
134+
// Reset to safe defaults
135+
workflowPerms := github.DefaultWorkflowPermissionOrganization{
136+
DefaultWorkflowPermissions: github.String("read"),
137+
CanApprovePullRequestReviews: github.Bool(false),
138+
}
139+
140+
_, resp, err := client.Actions.EditDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms)
141+
if err != nil {
142+
return handleEditWorkflowPermissionsError(err, resp)
143+
}
144+
145+
return nil
146+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
`, testOrganization)
20+
21+
check := resource.ComposeTestCheckFunc(
22+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testOrganization),
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+
testCase := func(t *testing.T, mode string) {
28+
resource.Test(t, resource.TestCase{
29+
PreCheck: func() { skipUnlessMode(t, mode) },
30+
Providers: testAccProviders,
31+
Steps: []resource.TestStep{
32+
{
33+
Config: config,
34+
Check: check,
35+
},
36+
},
37+
})
38+
}
39+
40+
t.Run("with an organization account", func(t *testing.T) {
41+
testCase(t, organization)
42+
})
43+
})
44+
45+
t.Run("updates organization workflow permissions without error", func(t *testing.T) {
46+
configs := map[string]string{
47+
"before": fmt.Sprintf(`
48+
resource "github_actions_organization_workflow_permissions" "test" {
49+
organization_slug = "%s"
50+
51+
default_workflow_permissions = "read"
52+
can_approve_pull_request_reviews = false
53+
}
54+
`, testOrganization),
55+
56+
"after": fmt.Sprintf(`
57+
resource "github_actions_organization_workflow_permissions" "test" {
58+
organization_slug = "%s"
59+
60+
default_workflow_permissions = "write" // This change might be restricted by the Enterprise's settings
61+
can_approve_pull_request_reviews = true
62+
}
63+
`, testOrganization),
64+
}
65+
66+
checks := map[string]resource.TestCheckFunc{
67+
"before": resource.ComposeTestCheckFunc(
68+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
69+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
70+
),
71+
"after": resource.ComposeTestCheckFunc(
72+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "write"),
73+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "true"),
74+
),
75+
}
76+
77+
testCase := func(t *testing.T, mode string) {
78+
resource.Test(t, resource.TestCase{
79+
PreCheck: func() { skipUnlessMode(t, mode) },
80+
Providers: testAccProviders,
81+
Steps: []resource.TestStep{
82+
{
83+
Config: configs["before"],
84+
Check: checks["before"],
85+
},
86+
{
87+
Config: configs["after"],
88+
Check: checks["after"],
89+
},
90+
},
91+
})
92+
}
93+
94+
t.Run("with an organization account", func(t *testing.T) {
95+
testCase(t, organization)
96+
})
97+
})
98+
99+
t.Run("imports enterprise workflow permissions without error", func(t *testing.T) {
100+
config := fmt.Sprintf(`
101+
resource "github_actions_organization_workflow_permissions" "test" {
102+
organization_slug = "%s"
103+
104+
default_workflow_permissions = "read"
105+
can_approve_pull_request_reviews = false
106+
}
107+
`, testOrganization)
108+
109+
check := resource.ComposeTestCheckFunc(
110+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testOrganization),
111+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"),
112+
resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"),
113+
)
114+
115+
testCase := func(t *testing.T, mode string) {
116+
resource.Test(t, resource.TestCase{
117+
PreCheck: func() { skipUnlessMode(t, mode) },
118+
Providers: testAccProviders,
119+
Steps: []resource.TestStep{
120+
{
121+
Config: config,
122+
Check: check,
123+
},
124+
{
125+
ResourceName: "github_actions_organization_workflow_permissions.test",
126+
ImportState: true,
127+
ImportStateVerify: true,
128+
},
129+
},
130+
})
131+
}
132+
133+
t.Run("with an organization account", func(t *testing.T) {
134+
testCase(t, organization)
135+
})
136+
})
137+
}

0 commit comments

Comments
 (0)