Skip to content

Commit

Permalink
Merge pull request #1762 from range-me/allow-saas-for-access-application
Browse files Browse the repository at this point in the history
Add SaaS Application support
  • Loading branch information
jacobbednarz committed Jul 26, 2022
2 parents 43e4b8d + db5d1d9 commit 5831ca8
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .changelog/1762.txt
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/cloudflare_access_application: Add support for Saas applications
```
18 changes: 16 additions & 2 deletions docs/resources/access_application.md
Expand Up @@ -52,7 +52,6 @@ resource "cloudflare_access_application" "staging_app" {

### Required

- `domain` (String) The complete URL of the asset you wish to put Cloudflare Access in front of. Can include subdomains or paths. Or both.
- `name` (String) Friendly name of the Access Application.

### Optional
Expand All @@ -64,14 +63,16 @@ resource "cloudflare_access_application" "staging_app" {
- `cors_headers` (Block List) CORS configuration for the Access Application. See below for reference structure. (see [below for nested schema](#nestedblock--cors_headers))
- `custom_deny_message` (String) Option that returns a custom error message when a user is denied access to the application.
- `custom_deny_url` (String) Option that redirects to a custom URL when a user is denied access to the application.
- `domain` (String) The complete URL of the asset you wish to put Cloudflare Access in front of. Can include subdomains or paths. Or both.
- `enable_binding_cookie` (Boolean) Option to provide increased security against compromised authorization tokens and CSRF attacks by requiring an additional "binding" cookie on requests. Defaults to `false`.
- `http_only_cookie_attribute` (Boolean) Option to add the `HttpOnly` cookie flag to access tokens. Defaults to `true`.
- `logo_url` (String) Image URL for the logo shown in the app launcher dashboard.
- `saas_app` (Block List, Max: 1) SaaS configuration for the Access Application. (see [below for nested schema](#nestedblock--saas_app))
- `same_site_cookie_attribute` (String) Defines the same-site cookie setting for access tokens. Available values: `none`, `lax`, `strict`.
- `service_auth_401_redirect` (Boolean) Option to return a 401 status code in service authentication rules on failed requests. Defaults to `false`.
- `session_duration` (String) How often a user will be forced to re-authorise. Must be in the format `48h` or `2h45m`. Defaults to `24h`.
- `skip_interstitial` (Boolean) Option to skip the authorization interstitial when using the CLI. Defaults to `false`.
- `type` (String) The application type. Available values: `self_hosted`, `ssh`, `vnc`, `file`. Defaults to `self_hosted`.
- `type` (String) The application type. Available values: `self_hosted`, `saas`, `ssh`, `vnc`, `file`. Defaults to `self_hosted`.
- `zone_id` (String) The zone identifier to target for the resource. Conflicts with `account_id`.

### Read-Only
Expand All @@ -93,6 +94,19 @@ Optional:
- `allowed_origins` (Set of String) List of origins permitted to make CORS requests.
- `max_age` (Number) The maximum time a preflight request will be cached.


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

Required:

- `consumer_service_url` (String) The service provider's endpoint that is responsible for receiving and parsing a SAML assertion.
- `sp_entity_id` (String) A globally unique name for an identity or service provider.

Optional:

- `name_id_format` (String) The format of the name identifier sent to the SaaS application. Defaults to `email`.

## Import

Import is supported using the following syntax:
Expand Down
4 changes: 4 additions & 0 deletions docs/resources/api_token.md
Expand Up @@ -30,6 +30,8 @@ resource "cloudflare_api_token" "api_token_create" {
resources = {
"com.cloudflare.api.user.${var.user_id}" = "*"
}
not_before = "2018-07-01T05:20:00Z"
expires_on = "2020-01-01T00:00:00Z"
}
condition {
Expand Down Expand Up @@ -144,6 +146,8 @@ resource "cloudflare_api_token" "dns_edit_all_account" {
### Optional

- `condition` (Block List, Max: 1) Conditions under which the token should be considered valid. (see [below for nested schema](#nestedblock--condition))
- `expires_on` (String) The expiration time on or after which the token MUST NOT be accepted for processing.
- `not_before` (String) The time before which the token MUST NOT be accepted for processing.

### Read-Only

Expand Down
18 changes: 18 additions & 0 deletions internal/provider/resource_cloudflare_access_application.go
Expand Up @@ -64,6 +64,10 @@ func resourceCloudflareAccessApplicationCreate(ctx context.Context, d *schema.Re
newAccessApplication.CorsHeaders = CORSConfig
}

if _, ok := d.GetOk("saas_app"); ok {
newAccessApplication.SaasApplication = convertSaasSchemaToStruct(d)
}

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

identifier, err := initIdentifier(d)
Expand Down Expand Up @@ -133,6 +137,11 @@ func resourceCloudflareAccessApplicationRead(ctx context.Context, d *schema.Reso
return diag.FromErr(fmt.Errorf("error setting Access Application CORS header configuration: %w", corsConfigErr))
}

saasConfig := convertSaasStructToSchema(d, accessApplication.SaasApplication)
if saasConfigErr := d.Set("saas_app", saasConfig); saasConfigErr != nil {
return diag.FromErr(fmt.Errorf("error setting Access Application SaaS app configuration: %w", saasConfigErr))
}

return nil
}

Expand Down Expand Up @@ -160,6 +169,10 @@ func resourceCloudflareAccessApplicationUpdate(ctx context.Context, d *schema.Re
ServiceAuth401Redirect: d.Get("service_auth_401_redirect").(bool),
}

if appType != "saas" {
updatedAccessApplication.Domain = d.Get("domain").(string)
}

if len(allowedIDPList) > 0 {
updatedAccessApplication.AllowedIdps = allowedIDPList
}
Expand All @@ -172,6 +185,11 @@ func resourceCloudflareAccessApplicationUpdate(ctx context.Context, d *schema.Re
updatedAccessApplication.CorsHeaders = CORSConfig
}

if _, ok := d.GetOk("saas_app"); ok {
saasConfig := convertSaasSchemaToStruct(d)
updatedAccessApplication.SaasApplication = saasConfig
}

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

identifier, err := initIdentifier(d)
Expand Down
49 changes: 49 additions & 0 deletions internal/provider/resource_cloudflare_access_application_test.go
Expand Up @@ -39,6 +39,7 @@ func TestAccCloudflareAccessApplication_BasicZone(t *testing.T) {
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
resource.TestCheckResourceAttr(name, "saas_app.#", "0"),
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
Expand Down Expand Up @@ -68,6 +69,7 @@ func TestAccCloudflareAccessApplication_BasicAccount(t *testing.T) {
resource.TestCheckResourceAttr(name, "type", "self_hosted"),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "cors_headers.#", "0"),
resource.TestCheckResourceAttr(name, "sass_app.#", "0"),
resource.TestCheckResourceAttr(name, "auto_redirect_to_identity", "false"),
),
},
Expand Down Expand Up @@ -106,6 +108,36 @@ func TestAccCloudflareAccessApplication_WithCORS(t *testing.T) {
})
}

func TestAccCloudflareAccessApplication_WithSaas(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)
testAccessAccPreCheck(t)
},
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckCloudflareAccessApplicationDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessApplicationConfigWithSaas(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "account_id", accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "type", "saas"),
resource.TestCheckResourceAttr(name, "session_duration", "24h"),
resource.TestCheckResourceAttr(name, "saas_app.#", "1"),
resource.TestCheckResourceAttr(name, "saas_app.0.sp_entity_id", "saas-app.example"),
resource.TestCheckResourceAttr(name, "saas_app.0.consumer_service_url", "https://saas-app.example/sso/saml/consume"),
resource.TestCheckResourceAttr(name, "saas_app.0.name_id_format", "email"),
),
},
},
})
}

func TestAccCloudflareAccessApplication_WithAutoRedirectToIdentity(t *testing.T) {
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_application.%s", rnd)
Expand Down Expand Up @@ -410,6 +442,23 @@ resource "cloudflare_access_application" "%[1]s" {
`, rnd, zoneID, domain)
}

func testAccCloudflareAccessApplicationConfigWithSaas(rnd, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_access_application" "%[1]s" {
account_id = "%[2]s"
name = "%[1]s"
type = "saas"
session_duration = "24h"
saas_app {
consumer_service_url = "https://saas-app.example/sso/saml/consume"
sp_entity_id = "saas-app.example"
name_id_format = "email"
}
auto_redirect_to_identity = false
}
`, rnd, accountID)
}

func testAccCloudflareAccessApplicationConfigWithAutoRedirectToIdentity(rnd, zoneID, domain string) string {
return fmt.Sprintf(`
resource "cloudflare_access_application" "%[1]s" {
Expand Down
59 changes: 56 additions & 3 deletions internal/provider/schema_cloudflare_access_application.go
Expand Up @@ -38,15 +38,16 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
},
"domain": {
Type: schema.TypeString,
Required: true,
Optional: true,
Computed: true,
Description: "The complete URL of the asset you wish to put Cloudflare Access in front of. Can include subdomains or paths. Or both.",
},
"type": {
Type: schema.TypeString,
Optional: true,
Default: "self_hosted",
ValidateFunc: validation.StringInSlice([]string{"self_hosted", "ssh", "vnc", "file"}, false),
Description: fmt.Sprintf("The application type. %s", renderAvailableDocumentationValuesStringSlice([]string{"self_hosted", "ssh", "vnc", "file"})),
ValidateFunc: validation.StringInSlice([]string{"self_hosted", "saas", "ssh", "vnc", "file"}, false),
Description: fmt.Sprintf("The application type. %s", renderAvailableDocumentationValuesStringSlice([]string{"self_hosted", "saas", "ssh", "vnc", "file"})),
},
"session_duration": {
Type: schema.TypeString,
Expand Down Expand Up @@ -121,6 +122,33 @@ func resourceCloudflareAccessApplicationSchema() map[string]*schema.Schema {
},
},
},
"saas_app": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "SaaS configuration for the Access Application.",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"sp_entity_id": {
Type: schema.TypeString,
Required: true,
Description: "A globally unique name for an identity or service provider.",
},
"consumer_service_url": {
Type: schema.TypeString,
Required: true,
Description: "The service provider's endpoint that is responsible for receiving and parsing a SAML assertion.",
},
"name_id_format": {
Type: schema.TypeString,
Optional: true,
Default: "email",
ValidateFunc: validation.StringInSlice([]string{"email", "id"}, false),
Description: "The format of the name identifier sent to the SaaS application.",
},
},
},
},
"auto_redirect_to_identity": {
Type: schema.TypeBool,
Optional: true,
Expand Down Expand Up @@ -261,3 +289,28 @@ func convertCORSStructToSchema(d *schema.ResourceData, headers *cloudflare.Acces

return []interface{}{m}
}

func convertSaasSchemaToStruct(d *schema.ResourceData) *cloudflare.SaasApplication {
SaasConfig := cloudflare.SaasApplication{}
if _, ok := d.GetOk("saas_app"); ok {
SaasConfig.SPEntityID = d.Get("saas_app.0.sp_entity_id").(string)
SaasConfig.ConsumerServiceUrl = d.Get("saas_app.0.consumer_service_url").(string)
SaasConfig.NameIDFormat = d.Get("saas_app.0.name_id_format").(string)
}

return &SaasConfig
}

func convertSaasStructToSchema(d *schema.ResourceData, app *cloudflare.SaasApplication) []interface{} {
if _, ok := d.GetOk("saas_app"); !ok {
return []interface{}{}
}

m := map[string]interface{}{
"sp_entity_id": app.SPEntityID,
"consumer_service_url": app.ConsumerServiceUrl,
"name_id_format": app.NameIDFormat,
}

return []interface{}{m}
}

0 comments on commit 5831ca8

Please sign in to comment.