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 support for Access Groups #510

Merged
merged 13 commits into from
Nov 19, 2019
1 change: 1 addition & 0 deletions cloudflare/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func Provider() terraform.ResourceProvider {
ResourcesMap: map[string]*schema.Resource{
"cloudflare_access_application": resourceCloudflareAccessApplication(),
"cloudflare_access_policy": resourceCloudflareAccessPolicy(),
"cloudflare_access_group": resourceCloudflareAccessGroup(),
"cloudflare_access_rule": resourceCloudflareAccessRule(),
"cloudflare_account_member": resourceCloudflareAccountMember(),
"cloudflare_argo": resourceCloudflareArgo(),
Expand Down
255 changes: 255 additions & 0 deletions cloudflare/resource_cloudflare_access_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package cloudflare

import (
"fmt"
"log"
"strings"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func resourceCloudflareAccessGroup() *schema.Resource {
return &schema.Resource{
Create: resourceCloudflareAccessGroupCreate,
Read: resourceCloudflareAccessGroupRead,
Update: resourceCloudflareAccessGroupUpdate,
Delete: resourceCloudflareAccessGroupDelete,
Importer: &schema.ResourceImporter{
State: resourceCloudflareAccessGroupImport,
},

Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Required: true,
matthieudolci marked this conversation as resolved.
Show resolved Hide resolved
},
"name": {
Type: schema.TypeString,
Required: true,
},
"require": {
Type: schema.TypeList,
Optional: true,
Elem: groupOptionElement,
},
"exclude": {
Type: schema.TypeList,
Optional: true,
Elem: groupOptionElement,
},
"include": {
Type: schema.TypeList,
Required: true,
Elem: groupOptionElement,
},
},
}
}

// groupOptionElement is used by `require`, `exclude` and `include`
// attributes to build out the expected access conditions.
var groupOptionElement = &schema.Resource{
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"email_domain": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"ip": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"everyone": {
Type: schema.TypeBool,
Optional: true,
},
"group": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}

func resourceCloudflareAccessGroupRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)

accessGroup, err := client.AccessGroup(accountID, d.Id())
if err != nil {
if strings.Contains(err.Error(), "HTTP status 404") {
log.Printf("[INFO] Access Group %s no longer exists", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("Error finding Access Group %q: %s", d.Id(), err)
}

d.Set("name", accessGroup.Name)
d.Set("require", accessGroup.Require)
d.Set("exclude", accessGroup.Exclude)
d.Set("include", accessGroup.Include)

return nil
}

func resourceCloudflareAccessGroupCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)
newAccessGroup := cloudflare.AccessGroup{
Name: d.Get("name").(string),
}

newAccessGroup = appendConditionalAccessGroupFields(newAccessGroup, d)

log.Printf("[DEBUG] Creating Cloudflare Access Group from struct: %+v", newAccessGroup)

accessGroup, err := client.CreateAccessGroup(accountID, newAccessGroup)
if err != nil {
return fmt.Errorf("error creating Access Group for ID %q: %s", accessGroup.ID, err)
}

d.SetId(accessGroup.ID)

matthieudolci marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

func resourceCloudflareAccessGroupUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)
updatedAccessGroup := cloudflare.AccessGroup{
Name: d.Get("name").(string),
ID: d.Id(),
}

updatedAccessGroup = appendConditionalAccessGroupFields(updatedAccessGroup, d)

log.Printf("[DEBUG] Updating Cloudflare Access Group from struct: %+v", updatedAccessGroup)

accessGroup, err := client.UpdateAccessGroup(accountID, updatedAccessGroup)
if err != nil {
return fmt.Errorf("error updating Access Group for ID %q: %s", d.Id(), err)
}

if accessGroup.ID == "" {
return fmt.Errorf("failed to find Access Group ID in update response; resource was empty")
}

return resourceCloudflareAccessGroupRead(d, meta)
}

func resourceCloudflareAccessGroupDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)

log.Printf("[DEBUG] Deleting Cloudflare Access Group using ID: %s", d.Id())

err := client.DeleteAccessGroup(accountID, d.Id())
if err != nil {
return fmt.Errorf("error deleting Access Group for ID %q: %s", d.Id(), err)
}

resourceCloudflareAccessGroupRead(d, meta)

return nil
}

func resourceCloudflareAccessGroupImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
attributes := strings.SplitN(d.Id(), "/", 3)
matthieudolci marked this conversation as resolved.
Show resolved Hide resolved

if len(attributes) != 3 {
matthieudolci marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/accessGroupID\"", d.Id())
}

accountID, accessGroupID := attributes[0], attributes[1]

log.Printf("[DEBUG] Importing Cloudflare Access Group: accountID %q, accessGroupID %q", accountID, accessGroupID)

d.Set("account_id", accountID)
d.SetId(accessGroupID)

resourceCloudflareAccessGroupRead(d, meta)

return []*schema.ResourceData{d}, nil
}

// appendConditionalAccessGroupFields determines which of the
// conditional group enforcement fields it should append to the
// AccessGroup by iterating over the provided values and generating the
// correct structs.
func appendConditionalAccessGroupFields(group cloudflare.AccessGroup, d *schema.ResourceData) cloudflare.AccessGroup {
exclude := d.Get("exclude").([]interface{})
for _, value := range exclude {
group.Exclude = buildAccessGroupCondition(value.(map[string]interface{}))
}

require := d.Get("require").([]interface{})
for _, value := range require {
group.Require = buildAccessGroupCondition(value.(map[string]interface{}))
}

include := d.Get("include").([]interface{})
for _, value := range include {
group.Include = buildAccessGroupCondition(value.(map[string]interface{}))
}

return group
}

// buildAccessGroupCondition iterates the provided `map` of values and
// generates the required (repetitive) structs.
//
// Returns the intended combination structure of AccessGroupEmail,
// AccessGroupEmailDomain, AccessGroupIP and AccessGroupEveryone
// structs.
func buildAccessGroupCondition(options map[string]interface{}) []interface{} {
var group []interface{}
for accessGroupType, values := range options {
// Since AccessGroupEveryone is a single boolean, we don't need to
// iterate over the values like the others so we treat it a little differently.
if accessGroupType == "everyone" {
if values == true {
log.Printf("[DEBUG] values for everyone %s", values)
group = append(group, cloudflare.AccessGroupEveryone{})
}
} else {
for _, value := range values.([]interface{}) {
switch accessGroupType {
case "email":
group = append(group, cloudflare.AccessGroupEmail{Email: struct {
Email string `json:"email"`
}{Email: value.(string)}})
case "email_domain":
group = append(group, cloudflare.AccessGroupEmailDomain{EmailDomain: struct {
Domain string `json:"domain"`
}{Domain: value.(string)}})
case "ip":
group = append(group, cloudflare.AccessGroupIP{IP: struct {
IP string `json:"ip"`
}{IP: value.(string)}})
case "group":
group = append(group, cloudflare.AccessGroupAccessGroup{Group: struct {
ID string `json:"id"`
}{ID: value.(string)}})
}
}
}
}

return group
}
3 changes: 3 additions & 0 deletions website/cloudflare.erb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
<li<%= sidebar_current("docs-cloudflare-resource-access-policy") %>>
<a href="/docs/providers/cloudflare/r/access_policy.html">cloudflare_access_policy</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-access-group") %>>
<a href="/docs/providers/cloudflare/r/access_group.html">cloudflare_access_group</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-access-rule") %>>
<a href="/docs/providers/cloudflare/r/access_rule.html">cloudflare_access_rule</a>
</li>
Expand Down
85 changes: 85 additions & 0 deletions website/docs/r/access_group.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
layout: "cloudflare"
page_title: "Cloudflare: cloudflare_access_group"
sidebar_current: "docs-cloudflare-resource-access-group"
description: |-
Provides a Cloudflare Access Group resource.
---

# cloudflare_access_group

Provides a Cloudflare Access Group resource. Access Groups are used
in conjunction with Access Plolicies to restrict access to a
matthieudolci marked this conversation as resolved.
Show resolved Hide resolved
particular resource.
matthieudolci marked this conversation as resolved.
Show resolved Hide resolved

## Example Usage

```hcl
# Allowing access to `test@example.com` email address only
jacobbednarz marked this conversation as resolved.
Show resolved Hide resolved
resource "cloudflare_access_group" "test_group" {
account_id = "975ecf5a45e3bcb680dba0722a420ad9"
name = "staging group"

include {
email = ["test@example.com"]
}
}

# Allowing `test@example.com` to access but only when coming from a
# specific IP.
resource "cloudflare_access_group" "test_group" {
account_id = "975ecf5a45e3bcb680dba0722a420ad9"
name = "staging group"

include {
email = ["test@example.com"]
}

require = {
ip = [var.office_ip]
}
}
```

## Argument Reference

The following arguments are supported:

* `account_id` - (Required) The ID of the account the group is
associated with.
* `name` - (Required) Friendly name of the Access Group.
* `require` - (Optional) A series of access conditions, see below for
full list.
* `exclude` - (Optional) A series of access conditions, see below for
full list.
* `include` - (Required) A series of access conditions, see below for
full list.

## Conditions

`require`, `exclude` and `include` arguments share the available
conditions which can be applied. The conditions are:

* `ip` - (Optional) A list of IP addresses or ranges. Example:
`ip = ["1.2.3.4", "10.0.0.0/2"]`
* `email` - (Optional) A list of email addresses. Example:
`email = ["test@example.com"]`
* `email_domain` - (Optional) A list of email domains. Example:
`email_domain = ["example.com"]`
* `everyone` - (Optional) Boolean indicating permitting access for all
requests. Example: `everyone = true`


## Import

Access Groups can be imported using a composite ID formed of account
ID and group ID.

```
$ terraform import cloudflare_access_group.staging 975ecf5a45e3bcb680dba0722a420ad9/67ea780ce4982c1cfbe6b7293afc765d
```

where

* `975ecf5a45e3bcb680dba0722a420ad9` - Account ID
* `67ea780ce4982c1cfbe6b7293afc765d` - Access Group ID