Skip to content

Commit

Permalink
Add access reusable policies support
Browse files Browse the repository at this point in the history
  • Loading branch information
Eduardo Gomes committed May 6, 2024
1 parent 815f78e commit 80fe48e
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 73 deletions.
7 changes: 7 additions & 0 deletions .changelog/3288.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/cloudflare_access_policies: added support for reusable policies
```

```release-note:enhancement
resource/cloudflare_access_application: added support for 'policies' argument
```
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,7 @@ require (
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace (
github.com/cloudflare/cloudflare-go v0.94.0 => "/home/egomes/cloudflare-go"
)
17 changes: 17 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_access_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re
}
}

if policies, ok := d.GetOk("policies"); ok {
newAccessApplication.Policies = expandInterfaceToStringList(policies)
}

tflog.Debug(ctx, fmt.Sprintf("Creating Cloudflare Access Application from struct: %+v", newAccessApplication))

identifier, err := initIdentifier(d)
Expand Down Expand Up @@ -212,6 +216,14 @@ func resourceCloudflareAccessApplicationRead(ctx context.Context, d *schema.Reso
d.Set("self_hosted_domains", accessApplication.SelfHostedDomains)
}

if _, ok := d.GetOk("policies"); ok {
policyIDs := make([]string, len(accessApplication.Policies))
for i := range accessApplication.Policies {
policyIDs[i] = accessApplication.Policies[i].ID
}
d.Set("policies", policyIDs)
}

return nil
}

Expand Down Expand Up @@ -260,6 +272,11 @@ func resourceCloudflareAccessApplicationUpdate(ctx context.Context, d *schema.Re
updatedAccessApplication.SelfHostedDomains = expandInterfaceToStringList(value.(*schema.Set).List())
}

if value, ok := d.GetOk("policies"); ok {
policies := expandInterfaceToStringList(value)
updatedAccessApplication.Policies = &policies
}

if _, ok := d.GetOk("cors_headers"); ok {
CORSConfig, err := convertCORSSchemaToStruct(d)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,31 @@ func TestAccCloudflareAccessApplication_WithDefinedTags(t *testing.T) {
})
}

func TestAccCloudflareAccessApplication_WithReusablePolicies(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_application.%s", rnd)
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheckAccount(t)
},
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessApplicationConfigWithReusablePolicies(rnd, domain, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "domain", fmt.Sprintf("%s.%s", rnd, domain)),
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
resource.TestCheckResourceAttr(name, "policies.#", "2"),
),
},
},
})
}

func TestAccCloudflareAccessApplication_WithAppLauncherCustomization(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_application.%s", rnd)
Expand Down Expand Up @@ -1311,7 +1336,40 @@ resource "cloudflare_access_application" "%[1]s" {
domain = "%[1]s.%[3]s"
type = "self_hosted"
session_duration = "24h"
tags = [cloudflare_access_tag.%[1]s.id]
tags = [cloudflare_access_tag.%[1]s.id]
}
`, rnd, zoneID, domain, accountID)
}

func testAccCloudflareAccessApplicationConfigWithReusablePolicies(rnd, domain string, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_access_policy" "%[1]s_p1" {
account_id = "%[3]s"
name = "%[1]s"
decision = "allow"
include {
email = ["[email protected]"]
}
}
resource "cloudflare_access_policy" "%[1]s_p2" {
account_id = "%[3]s"
name = "%[1]s"
decision = "non_identity"
include {
ip = ["127.0.0.1/32"]
}
}
resource "cloudflare_access_application" "%[1]s" {
account_id = "%[3]s"
name = "%[1]s"
domain = "%[1]s.%[2]s"
type = "self_hosted"
policies = [
cloudflare_access_policy.%[1]s_p1.id,
cloudflare_access_policy.%[1]s_p2.id
]
}
`, rnd, domain, accountID)
}
125 changes: 72 additions & 53 deletions internal/sdkv2provider/resource_cloudflare_access_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,15 @@ func schemaAccessPolicyApprovalGroupToAPI(data map[string]interface{}) cloudflar
return approvalGroup
}

func resourceCloudflareAccessPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
appID := d.Get("application_id").(string)

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

accessPolicy, err := client.GetAccessPolicy(ctx, identifier, cloudflare.GetAccessPolicyParams{ApplicationID: appID, PolicyID: d.Id()})
if err != nil {
var notFoundError *cloudflare.NotFoundError
if errors.As(err, &notFoundError) {
tflog.Info(ctx, fmt.Sprintf("Access Policy %s no longer exists", d.Id()))
d.SetId("")
return nil
}
return diag.FromErr(fmt.Errorf("error finding Access Policy %q: %w", d.Id(), err))
func apiCloudflareAccessPolicyToResource(ctx context.Context, d *schema.ResourceData, appID string, accessPolicy cloudflare.AccessPolicy) diag.Diagnostics {
if appID != "" {
// policy is tied to a single application (legacy) and its execution precedence
// within the app can be set.
d.Set("precedence", accessPolicy.Precedence)
}

d.Set("name", accessPolicy.Name)
d.Set("decision", accessPolicy.Decision)
d.Set("precedence", accessPolicy.Precedence)

if err := d.Set("require", TransformAccessGroupForSchema(ctx, accessPolicy.Require)); err != nil {
return diag.FromErr(fmt.Errorf("failed to set require attribute: %w", err))
Expand Down Expand Up @@ -112,17 +98,42 @@ func resourceCloudflareAccessPolicyRead(ctx context.Context, d *schema.ResourceD
return diag.FromErr(fmt.Errorf("failed to set approval_group attribute: %w", err))
}
}

return nil
}

func resourceCloudflareAccessPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

appID, _ := d.Get("application_id").(string)
policy, err := client.GetAccessPolicy(ctx, identifier, cloudflare.GetAccessPolicyParams{
ApplicationID: appID,
PolicyID: d.Id(),
})
if err != nil {
var notFoundError *cloudflare.NotFoundError
if errors.As(err, &notFoundError) {
tflog.Info(ctx, fmt.Sprintf("Access Policy %s no longer exists", d.Id()))
d.SetId("")
return nil
}
return diag.FromErr(fmt.Errorf("error finding Access Policy %q: %w", d.Id(), err))
}
return apiCloudflareAccessPolicyToResource(ctx, d, appID, policy)
}

func resourceCloudflareAccessPolicyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
appID := d.Get("application_id").(string)
appID, _ := d.Get("application_id").(string)
precedence, _ := d.Get("precedence").(int)
newAccessPolicy := cloudflare.CreateAccessPolicyParams{
ApplicationID: appID,
Precedence: precedence,
Name: d.Get("name").(string),
Precedence: d.Get("precedence").(int),
Decision: d.Get("decision").(string),
SessionDuration: cloudflare.StringPtr(d.Get("session_duration").(string)),
}
Expand Down Expand Up @@ -175,96 +186,95 @@ func resourceCloudflareAccessPolicyCreate(ctx context.Context, d *schema.Resourc

accessPolicy, err := client.CreateAccessPolicy(ctx, identifier, newAccessPolicy)
if err != nil {
return diag.FromErr(fmt.Errorf("error creating Access Policy for ID %q: %w", accessPolicy.ID, err))
return diag.FromErr(fmt.Errorf("error creating Access Policy for ID %q: %w", d.Id(), err))
}

d.SetId(accessPolicy.ID)

return nil
return apiCloudflareAccessPolicyToResource(ctx, d, appID, accessPolicy)
}

func resourceCloudflareAccessPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
appID := d.Get("application_id").(string)
updatedAccessPolicy := cloudflare.UpdateAccessPolicyParams{
appID, _ := d.Get("application_id").(string)
precedence, _ := d.Get("precedence").(int)
updateReq := cloudflare.UpdateAccessPolicyParams{
ApplicationID: appID,
Precedence: precedence,
PolicyID: d.Id(),
Name: d.Get("name").(string),
Precedence: d.Get("precedence").(int),
Decision: d.Get("decision").(string),
SessionDuration: cloudflare.StringPtr(d.Get("session_duration").(string)),
}

exclude := d.Get("exclude").([]interface{})
for _, value := range exclude {
if value != nil {
updatedAccessPolicy.Exclude = BuildAccessGroupCondition(value.(map[string]interface{}))
updateReq.Exclude = BuildAccessGroupCondition(value.(map[string]interface{}))
}
}

require := d.Get("require").([]interface{})
for _, value := range require {
if value != nil {
updatedAccessPolicy.Require = BuildAccessGroupCondition(value.(map[string]interface{}))
updateReq.Require = BuildAccessGroupCondition(value.(map[string]interface{}))
}
}

include := d.Get("include").([]interface{})
for _, value := range include {
if value != nil {
updatedAccessPolicy.Include = BuildAccessGroupCondition(value.(map[string]interface{}))
updateReq.Include = BuildAccessGroupCondition(value.(map[string]interface{}))
}
}

isolationRequired := d.Get("isolation_required").(bool)
updatedAccessPolicy.IsolationRequired = &isolationRequired
updateReq.IsolationRequired = &isolationRequired

purposeJustificationRequired := d.Get("purpose_justification_required").(bool)
updatedAccessPolicy.PurposeJustificationRequired = &purposeJustificationRequired
updateReq.PurposeJustificationRequired = &purposeJustificationRequired

purposeJustificationPrompt := d.Get("purpose_justification_prompt").(string)
updatedAccessPolicy.PurposeJustificationPrompt = &purposeJustificationPrompt
updateReq.PurposeJustificationPrompt = &purposeJustificationPrompt

approvalRequired := d.Get("approval_required").(bool)
updatedAccessPolicy.ApprovalRequired = &approvalRequired
updateReq.ApprovalRequired = &approvalRequired

approvalGroups := d.Get("approval_group").([]interface{})
for _, approvalGroup := range approvalGroups {
approvalGroupAsMap := approvalGroup.(map[string]interface{})
updatedAccessPolicy.ApprovalGroups = append(updatedAccessPolicy.ApprovalGroups, schemaAccessPolicyApprovalGroupToAPI(approvalGroupAsMap))
updateReq.ApprovalGroups = append(updateReq.ApprovalGroups, schemaAccessPolicyApprovalGroupToAPI(approvalGroupAsMap))
}

tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Access Policy from struct: %+v", updatedAccessPolicy))
tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Access Policy from struct: %+v", updateReq))

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

accessPolicy, err := client.UpdateAccessPolicy(ctx, identifier, updatedAccessPolicy)
updatedPolicy, err := client.UpdateAccessPolicy(ctx, identifier, updateReq)
if err != nil {
return diag.FromErr(fmt.Errorf("error updating Access Policy for ID %q: %w", d.Id(), err))
}

if accessPolicy.ID == "" {
return diag.FromErr(fmt.Errorf("failed to find Access Policy ID in update response; resource was empty"))
}

return resourceCloudflareAccessPolicyRead(ctx, d, meta)
return apiCloudflareAccessPolicyToResource(ctx, d, updateReq.ApplicationID, updatedPolicy)
}

func resourceCloudflareAccessPolicyDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
appID := d.Get("application_id").(string)

tflog.Debug(ctx, fmt.Sprintf("Deleting Cloudflare Access Policy using ID: %s", d.Id()))

identifier, err := initIdentifier(d)
if err != nil {
return diag.FromErr(err)
}

err = client.DeleteAccessPolicy(ctx, identifier, cloudflare.DeleteAccessPolicyParams{ApplicationID: appID, PolicyID: d.Id()})
appID, _ := d.Get("application_id").(string)
tflog.Debug(ctx, fmt.Sprintf("Deleting Cloudflare Access Policy using ID: %s", d.Id()))
err = client.DeleteAccessPolicy(ctx, identifier, cloudflare.DeleteAccessPolicyParams{
ApplicationID: appID,
PolicyID: d.Id(),
})
if err != nil {
return diag.FromErr(fmt.Errorf("error deleting Access Policy for ID %q: %w", d.Id(), err))
}
Expand All @@ -277,23 +287,32 @@ func resourceCloudflareAccessPolicyDelete(ctx context.Context, d *schema.Resourc
func resourceCloudflareAccessPolicyImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
attributes := strings.SplitN(d.Id(), "/", 4)

if len(attributes) != 4 {
if len(attributes) < 3 {
return nil, fmt.Errorf(
"invalid id (%q) specified, should be in format %q or %q",
"invalid id (%q) specified, should be in format %q, %q or %q",
d.Id(),
"account/accountID/accessPolicyID",
"account/accountID/accessApplicationID/accessPolicyID",
"zone/zoneID/accessApplicationID/accessPolicyID",
)
}

identifierType, identifierID, accessAppID, accessPolicyID := attributes[0], attributes[1], attributes[2], attributes[3]

tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Access Policy: %s %q, appID %q, accessPolicyID %q", identifierType, identifierID, accessAppID, accessPolicyID))
identifierType, identifierID := attributes[0], attributes[1]
if len(attributes) == 4 {
// Legacy policy tied to a single application.
accessAppID, accessPolicyID := attributes[2], attributes[3]
tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Access Policy: %s %q, appID %q, accessPolicyID %q", identifierType, identifierID, accessAppID, accessPolicyID))
d.Set("application_id", accessAppID)
d.SetId(accessPolicyID)
} else {
// Standalone reusable policy
accessPolicyID := attributes[2]
tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Access Policy: %s %q, accessPolicyID %q", identifierType, identifierID, accessPolicyID))
d.SetId(accessPolicyID)
}

//lintignore:R001
d.Set(fmt.Sprintf("%s_id", identifierType), identifierID)
d.Set("application_id", accessAppID)
d.SetId(accessPolicyID)

resourceCloudflareAccessPolicyRead(ctx, d, meta)

Expand Down
Loading

0 comments on commit 80fe48e

Please sign in to comment.