Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Access organization #1961

Merged
merged 7 commits into from
Oct 19, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/1961.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
cloudflare_access_organization
```
65 changes: 65 additions & 0 deletions docs/resources/access_organization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
page_title: "cloudflare_access_organization Resource - Cloudflare"
subcategory: ""
description: |-
A Zero Trust organization defines the user login experience.
---

# cloudflare_access_organization (Resource)

A Zero Trust organization defines the user login experience.

## Example Usage

```terraform
resource "cloudflare_access_organization" "example" {
account_id = "f037e56e89293a057740de681ac9abbe"
name = "example.cloudflareaccess.com"
auth_domain = "example.cloudflareaccess.com"
is_ui_read_only = false

login_design {
background_color = "#ffffff"
text_color = "#000000"
logo_path = "https://example.com/logo.png"
header_text = "My header text"
footer_text = "My footer text"
}
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `auth_domain` (String) The unique subdomain assigned to your Zero Trust organization.

### Optional

- `account_id` (String) The account identifier to target for the resource. Conflicts with `zone_id`.
- `is_ui_read_only` (Boolean) When set to true, this will disable all editing of Access resources via the Zero Trust Dashboard.
- `login_design` (Block List) (see [below for nested schema](#nestedblock--login_design))
- `name` (String) The name of your Zero Trust organization.
- `zone_id` (String) The zone identifier to target for the resource. Conflicts with `account_id`.

### Read-Only

- `id` (String) The ID of this resource.

<a id="nestedblock--login_design"></a>
### Nested Schema for `login_design`

Optional:

- `background_color` (String) The background color on the login page.
- `footer_text` (String) The text at the bottom of the login page.
- `header_text` (String) The text at the top of the login page.
- `logo_path` (String) The URL of the logo on the login page.
- `text_color` (String) The text color on the login page.

## Import

Import is supported using the following syntax:
```shell
$ terraform import cloudflare_access_organization.example <account_id>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$ terraform import cloudflare_access_organization.example <account_id>
14 changes: 14 additions & 0 deletions examples/resources/cloudflare_access_organization/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "cloudflare_access_organization" "example" {
account_id = "f037e56e89293a057740de681ac9abbe"
name = "example.cloudflareaccess.com"
auth_domain = "example.cloudflareaccess.com"
is_ui_read_only = false

login_design {
background_color = "#ffffff"
text_color = "#000000"
logo_path = "https://example.com/logo.png"
header_text = "My header text"
footer_text = "My footer text"
}
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_access_identity_provider": resourceCloudflareAccessIdentityProvider(),
"cloudflare_access_keys_configuration": resourceCloudflareAccessKeysConfiguration(),
"cloudflare_access_mutual_tls_certificate": resourceCloudflareAccessMutualTLSCertificate(),
"cloudflare_access_organization": resourceCloudflareAccessOrganization(),
"cloudflare_access_policy": resourceCloudflareAccessPolicy(),
"cloudflare_access_rule": resourceCloudflareAccessRule(),
"cloudflare_access_service_token": resourceCloudflareAccessServiceToken(),
Expand Down
118 changes: 118 additions & 0 deletions internal/provider/resource_cloudflare_access_organization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package provider

import (
"context"
"errors"
"fmt"

"github.com/MakeNowJust/heredoc/v2"
"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

type contextKey int

const orgAccessImportCtxKey contextKey = iota

func resourceCloudflareAccessOrganization() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareAccessOrganizationSchema(),
CreateContext: resourceCloudflareAccessOrganizationCreate,
ReadContext: resourceCloudflareAccessOrganizationRead,
UpdateContext: resourceCloudflareAccessOrganizationUpdate,
DeleteContext: resourceCloudflareAccessOrganizationNoop,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflareAccessOrganizationImport,
},
Description: heredoc.Doc(`
A Zero Trust organization defines the user login experience.
`),
}
}

func resourceCloudflareAccessOrganizationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return diag.FromErr(fmt.Errorf("access organizations cannot be created and must be imported"))
Justin-Holmes marked this conversation as resolved.
Show resolved Hide resolved
}

func resourceCloudflareAccessOrganizationNoop(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return nil
Justin-Holmes marked this conversation as resolved.
Show resolved Hide resolved
}

func resourceCloudflareAccessOrganizationRead(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)
}

var organization cloudflare.AccessOrganization
if identifier.Type == AccountType {
organization, _, err = client.AccessOrganization(ctx, identifier.Value)
} else {
organization, _, err = client.ZoneLevelAccessOrganization(ctx, identifier.Value)
}
if err != nil {
return diag.FromErr(fmt.Errorf("error fetching access organization: %w", err))
}

d.Set("name", organization.Name)
d.Set("auth_domain", organization.AuthDomain)
d.Set("is_ui_read_only", organization.IsUIReadOnly)

loginDesign := convertLoginDesignStructToSchema(ctx, d, &organization.LoginDesign)
if loginDesignErr := d.Set("login_design", loginDesign); loginDesignErr != nil {
return diag.FromErr(fmt.Errorf("error setting Access Organization Login Design configuration: %w", loginDesignErr))
}

return nil
}

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

updatedAccessOrganization := cloudflare.AccessOrganization{
Name: d.Get("name").(string),
AuthDomain: d.Get("auth_domain").(string),
IsUIReadOnly: cloudflare.BoolPtr(d.Get("is_ui_read_only").(bool)),
}
loginDesign := convertLoginDesignSchemaToStruct(d)
updatedAccessOrganization.LoginDesign = *loginDesign

tflog.Debug(ctx, fmt.Sprintf("Updating Cloudflare Access Organization from struct: %+v", updatedAccessOrganization))

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

if identifier.Type == AccountType {
_, err = client.UpdateAccessOrganization(ctx, identifier.Value, updatedAccessOrganization)
} else {
_, err = client.UpdateZoneLevelAccessOrganization(ctx, identifier.Value, updatedAccessOrganization)
}
if err != nil {
return diag.FromErr(fmt.Errorf("error updating Access Organization for %s %q: %w", identifier.Type, identifier.Value, err))
}

return resourceCloudflareAccessOrganizationRead(ctx, d, meta)
}

func resourceCloudflareAccessOrganizationImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
ctx = context.WithValue(ctx, orgAccessImportCtxKey, true)

accountID := d.Id()

tflog.Info(ctx, fmt.Sprintf("Importing Cloudflare Access Organization for account %s", accountID))

d.Set("account_id", accountID)

readErr := resourceCloudflareAccessOrganizationRead(ctx, d, meta)
if readErr != nil {
return nil, errors.New("failed to read Access Organization state")
}

return []*schema.ResourceData{d}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package provider

import (
"fmt"
"testing"

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

func TestAccCloudflareAccessOrganization(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_organization.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheck(t)
testAccessAccPreCheck(t)
},
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessOrganizationConfigBasic(rnd, accountID),
ResourceName: name,
ImportState: true,
ImportStateId: accountID,
ImportStateCheck: accessOrgImportStateCheck,
},
},
})
}

func accessOrgImportStateCheck(instanceStates []*terraform.InstanceState) error {
state := instanceStates[0]
attrs := state.Attributes

stateChecks := []struct {
field string
stateValue string
expectedValue string
}{
{field: "ID", stateValue: state.ID, expectedValue: accountID},
{field: "account_id", stateValue: attrs["account_id"], expectedValue: accountID},
{field: "name", stateValue: attrs["name"], expectedValue: "terraform-cfapi.cloudflareaccess.com"},
{field: "auth_domain", stateValue: attrs["auth_domain"], expectedValue: "terraform-cfapi.cloudflareaccess.com"},
{field: "is_ui_read_only", stateValue: attrs["is_ui_read_only"], expectedValue: "false"},
{field: "login_design.#", stateValue: attrs["login_design.#"], expectedValue: "1"},
}

for _, check := range stateChecks {
if check.stateValue != check.expectedValue {
return fmt.Errorf("%s has value %s and does not match expected value %s", check.field, check.stateValue, check.expectedValue)
}
}

return nil
}

func testAccCloudflareAccessOrganizationConfigBasic(rnd, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_access_organization" "%[1]s" {
account_id = "%[2]s"
name = "terraform-cfapi.cloudflareaccess.com"
auth_domain = "terraform-cfapi.cloudflareaccess.com1"
is_ui_read_only = false

login_design {
background_color = "#FFFFFF"
text_color = "#000000"
logo_path = "https://example.com/logo.png"
header_text = "My header text"
footer_text = "My footer text"
}
}
`, rnd, accountID)
}