diff --git a/.changelog/1961.txt b/.changelog/1961.txt
new file mode 100644
index 0000000000..50ff5d91bf
--- /dev/null
+++ b/.changelog/1961.txt
@@ -0,0 +1,3 @@
+```release-note:new-resource
+cloudflare_access_organization
+```
diff --git a/docs/resources/access_organization.md b/docs/resources/access_organization.md
new file mode 100644
index 0000000000..ff94d0bd55
--- /dev/null
+++ b/docs/resources/access_organization.md
@@ -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
+
+### 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.
+
+
+### 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
+```
diff --git a/examples/resources/cloudflare_access_organization/import.sh b/examples/resources/cloudflare_access_organization/import.sh
new file mode 100644
index 0000000000..5024f7c658
--- /dev/null
+++ b/examples/resources/cloudflare_access_organization/import.sh
@@ -0,0 +1 @@
+$ terraform import cloudflare_access_organization.example
diff --git a/examples/resources/cloudflare_access_organization/resource.tf b/examples/resources/cloudflare_access_organization/resource.tf
new file mode 100644
index 0000000000..73ccaec3dc
--- /dev/null
+++ b/examples/resources/cloudflare_access_organization/resource.tf
@@ -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"
+ }
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index b774ed87d9..830bbccf95 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -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(),
diff --git a/internal/provider/resource_cloudflare_access_organization.go b/internal/provider/resource_cloudflare_access_organization.go
new file mode 100644
index 0000000000..c05d88a520
--- /dev/null
+++ b/internal/provider/resource_cloudflare_access_organization.go
@@ -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"))
+}
+
+func resourceCloudflareAccessOrganizationNoop(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ return nil
+}
+
+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
+}
diff --git a/internal/provider/resource_cloudflare_access_organization_test.go b/internal/provider/resource_cloudflare_access_organization_test.go
new file mode 100644
index 0000000000..fb8f8bb332
--- /dev/null
+++ b/internal/provider/resource_cloudflare_access_organization_test.go
@@ -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)
+}
diff --git a/internal/provider/schema_cloudflare_access_organization.go b/internal/provider/schema_cloudflare_access_organization.go
new file mode 100644
index 0000000000..f5b38fc864
--- /dev/null
+++ b/internal/provider/schema_cloudflare_access_organization.go
@@ -0,0 +1,111 @@
+package provider
+
+import (
+ "context"
+
+ "github.com/cloudflare/cloudflare-go"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+)
+
+func resourceCloudflareAccessOrganizationSchema() map[string]*schema.Schema {
+ return map[string]*schema.Schema{
+ "account_id": {
+ Description: "The account identifier to target for the resource.",
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ ConflictsWith: []string{"zone_id"},
+ },
+ "zone_id": {
+ Description: "The zone identifier to target for the resource.",
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ ConflictsWith: []string{"account_id"},
+ },
+ "auth_domain": {
+ Description: "The unique subdomain assigned to your Zero Trust organization.",
+ Type: schema.TypeString,
+ Required: true,
+ },
+ "name": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The name of your Zero Trust organization.",
+ },
+ "is_ui_read_only": {
+ Type: schema.TypeBool,
+ Optional: true,
+ Description: "When set to true, this will disable all editing of Access resources via the Zero Trust Dashboard",
+ },
+ "login_design": {
+ Type: schema.TypeList,
+ Optional: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "background_color": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The background color on the login page",
+ },
+ "text_color": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The text color on the login page",
+ },
+ "logo_path": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The URL of the logo on the login page",
+ },
+ "header_text": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The text at the top of the login page",
+ },
+ "footer_text": {
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "The text at the bottom of the login page",
+ },
+ },
+ },
+ },
+ }
+}
+
+func convertLoginDesignSchemaToStruct(d *schema.ResourceData) *cloudflare.AccessOrganizationLoginDesign {
+ LoginDesign := cloudflare.AccessOrganizationLoginDesign{}
+
+ if _, ok := d.GetOk("login_design.0"); ok {
+ LoginDesign.BackgroundColor = d.Get("login_design.0.background_color").(string)
+ LoginDesign.TextColor = d.Get("login_design.0.text_color").(string)
+ LoginDesign.LogoPath = d.Get("login_design.0.logo_path").(string)
+ LoginDesign.HeaderText = d.Get("login_design.0.header_text").(string)
+ LoginDesign.FooterText = d.Get("login_design.0.footer_text").(string)
+ }
+
+ return &LoginDesign
+}
+
+func convertLoginDesignStructToSchema(ctx context.Context, d *schema.ResourceData, loginDesign *cloudflare.AccessOrganizationLoginDesign) []interface{} {
+ var onImport bool
+ var ok bool
+ if onImport, ok = ctx.Value(orgAccessImportCtxKey).(bool); !ok {
+ onImport = false
+ }
+
+ if _, ok := d.GetOk("login_design"); !ok && !onImport {
+ return []interface{}{}
+ }
+
+ m := map[string]interface{}{
+ "background_color": loginDesign.BackgroundColor,
+ "text_color": loginDesign.TextColor,
+ "logo_path": loginDesign.LogoPath,
+ "header_text": loginDesign.HeaderText,
+ "footer_text": loginDesign.FooterText,
+ }
+
+ return []interface{}{m}
+}