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 SCIM Integration Resource #556

Merged
merged 15 commits into from
Jun 4, 2021
50 changes: 50 additions & 0 deletions docs/resources/scim_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_scim_integration Resource - terraform-provider-snowflake"
subcategory: ""
description: |-

---

# snowflake_scim_integration (Resource)



## Example Usage

```terraform
resource "snowflake_scim_integration" "aad" {
name = "AAD_PROVISIONING"
network_policy = "AAD_NETWORK_POLICY"
run_as_role = "AAD_PROVISIONER"
scim_client = "AZURE"
enabled = true
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- **name** (String) Specifies the name of the SCIM integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.
- **run_as_role** (String) Specify the SCIM role in Snowflake that owns any users and roles that are imported from the identity provider into Snowflake using SCIM.
- **scim_client** (String) Specifies the client type for the scim integration.

### Optional

- **network_policy** (String) Specifies an existing network policy active for your account. The network policy restricts the list of user IP addresses when exchanging an authorization code for an access or refresh token and when using a refresh token to obtain a new access token. If this parameter is not set, the network policy for the account (if any) is used instead.
- **enabled** (Boolean) Specifies whether this SCIM integration is enabled or disabled. If the SCIM integration is disabled, any external function that relies on it will not work.
- **id** (String) The ID of this resource.

### Read-Only

- **created_on** (String) Date and time when the SCIM integration was created.

## Import

Import is supported using the following syntax:

```shell
terraform import snowflake_scim_integration.example name
```
1 change: 1 addition & 0 deletions examples/resources/snowflake_scim_integration/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import snowflake_scim_integration.example name
7 changes: 7 additions & 0 deletions examples/resources/snowflake_scim_integration/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "snowflake_scim_integration" "aad" {
name = "AAD_PROVISIONING"
network_policy = "AAD_NETWORK_POLICY"
run_as_role = "AAD_PROVISIONER"
scim_client = "AZURE"
enabled = true
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_role": resources.Role(),
"snowflake_role_grants": resources.RoleGrants(),
"snowflake_schema": resources.Schema(),
"snowflake_scim_integration": resources.SCIMIntegration(),
"snowflake_share": resources.Share(),
"snowflake_stage": resources.Stage(),
"snowflake_storage_integration": resources.StorageIntegration(),
Expand Down
8 changes: 8 additions & 0 deletions pkg/resources/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@ func apiIntegration(t *testing.T, id string, params map[string]interface{}) *sch
return d
}

func scimIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.SCIMIntegration().Schema, params)
r.NotNil(d)
d.SetId(id)
return d
}

func externalFunction(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.ExternalFunction().Schema, params)
Expand Down
229 changes: 229 additions & 0 deletions pkg/resources/scim_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package resources

import (
"database/sql"
"fmt"
"log"
"strings"

"github.com/chanzuckerberg/terraform-provider-snowflake/pkg/snowflake"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

var scimIntegrationSchema = map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Specifies the name of the SCIM integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.",
},
"scim_client": {
Type: schema.TypeString,
Required: true,
Description: "Specifies the client type for the scim integration",
ValidateFunc: validation.StringInSlice([]string{
"OKTA", "AZURE", "CUSTOM",
}, true),
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
normalize := func(s string) string {
return strings.ToUpper(strings.Replace(s, "-", "", -1))
}
return normalize(old) == normalize(new)
},
},
"run_as_role": {
ChrisIsidora marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Required: true,
Description: "Specify the SCIM role in Snowflake that owns any users and roles that are imported from the identity provider into Snowflake using SCIM.",
ValidateFunc: validation.StringInSlice([]string{
"OKTA_PROVISIONER", "AAD_PROVISIONER", "GENERIC_SCIM_PROVISIONER",
}, true),
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
normalize := func(s string) string {
return strings.ToUpper(strings.Replace(s, "-", "", -1))
}
return normalize(old) == normalize(new)
},
},
"network_policy": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies an existing network policy active for your account. The network policy restricts the list of user IP addresses when exchanging an authorization code for an access or refresh token and when using a refresh token to obtain a new access token. If this parameter is not set, the network policy for the account (if any) is used instead.",
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: "Specifies whether this SCIM integration is enabled or disabled. If the SCIM integration is disabled, any external function that relies on it will not work.",
},
"created_on": {
Type: schema.TypeString,
Computed: true,
Description: "Date and time when the SCIM integration was created.",
},
}

// SCIMIntegration returns a pointer to the resource representing a network policy
func SCIMIntegration() *schema.Resource {
return &schema.Resource{
Create: CreateSCIMIntegration,
Read: ReadSCIMIntegration,
Update: UpdateSCIMIntegration,
Delete: DeleteSCIMIntegration,

Schema: scimIntegrationSchema,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
}
}

// CreateSCIMIntegration implements schema.CreateFunc
func CreateSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := d.Get("name").(string)

stmt := snowflake.ScimIntegration(name).Create()

// Set required fields
stmt.SetRaw(`TYPE=SCIM`)
stmt.SetBool(`ENABLED`, d.Get("enabled").(bool))
stmt.SetString(`SCIM_CLIENT`, d.Get("scim_client").(string))
stmt.SetString(`RUN_AS_ROLE`, d.Get("run_as_role").(string))

// Set optional fields
if _, ok := d.GetOk("network_policy"); ok {
stmt.SetString(`NETWORK_POLICY`, d.Get("network_policy").(string))
}

err := snowflake.Exec(db, stmt.Statement())
if err != nil {
return fmt.Errorf("error creating security integration: %w", err)
ChrisIsidora marked this conversation as resolved.
Show resolved Hide resolved
}

d.SetId(name)

return ReadSCIMIntegration(d, meta)
}

// ReadSCIMIntegration implements schema.ReadFunc
func ReadSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
id := d.Id()

stmt := snowflake.ScimIntegration(id).Show()
row := snowflake.QueryRow(db, stmt)

// Some properties can come from the SHOW INTEGRATION call

s, err := snowflake.ScanScimIntegration(row)
if err != nil {
return fmt.Errorf("could not show security integration: %w", err)
ChrisIsidora marked this conversation as resolved.
Show resolved Hide resolved
}

// Note: category must be Security or something is broken
if c := s.Category.String; c != "SECURITY" {
return fmt.Errorf("expected %v to be an Security integration, got %v", id, c)
}

if err := d.Set("scim_client", strings.TrimPrefix(s.IntegrationType.String, "SCIM -")); err != nil {
return err
}

if err := d.Set("name", s.Name.String); err != nil {
return err
}

if err := d.Set("created_on", s.CreatedOn.String); err != nil {
return err
}

if err := d.Set("enabled", s.Enabled.Bool); err != nil {
return err
}

// Some properties come from the DESCRIBE INTEGRATION call
// We need to grab them in a loop
var k, pType string
var v, unused interface{}
stmt = snowflake.ScimIntegration(id).Describe()
rows, err := db.Query(stmt)
if err != nil {
return fmt.Errorf("could not describe security integration: %w", err)
ChrisIsidora marked this conversation as resolved.
Show resolved Hide resolved
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(&k, &pType, &v, &unused); err != nil {
ChrisIsidora marked this conversation as resolved.
Show resolved Hide resolved
return err
}
switch k {
case "ENABLED":
// We set this using the SHOW INTEGRATION call so let's ignore it here
case "NETWORK_POLICY":
if err = d.Set("network_policy", v.(string)); err != nil {
return err
}
case "RUN_AS_ROLE":
if err = d.Set("run_as_role", v.(string)); err != nil {
return err
}
default:
log.Printf("[WARN] unexpected security integration property %v returned from Snowflake", k)
}
}

return err
}

// UpdateSCIMIntegration implements schema.UpdateFunc
func UpdateSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
id := d.Id()

stmt := snowflake.ScimIntegration(id).Alter()

var runSetStatement bool

if d.HasChange("enabled") {
runSetStatement = true
stmt.SetBool(`ENABLED`, d.Get("enabled").(bool))
}

if d.HasChange("scim_client") {
runSetStatement = true
stmt.SetString(`SCIM_CLIENT`, d.Get("scim_client").(string))
}

if d.HasChange("run_as_role") {
runSetStatement = true
stmt.SetString(`RUN_AS_ROLE`, d.Get("run_as_role").(string))
}

// We need to UNSET this if we remove all api blocked prefixes.
if d.HasChange("network_policy") {
v := d.Get("network_policy").(string)
if len(v) == 0 {
err := snowflake.Exec(db, fmt.Sprintf(`ALTER SECURITY INTEGRATION %v UNSET NETWORK_POLICY`, id))
if err != nil {
return fmt.Errorf("error unsetting network_policy: %w", err)
}
} else {
runSetStatement = true
stmt.SetString(`NETWORK_POLICY`, d.Get("network_policy").(string))
}
}

if runSetStatement {
if err := snowflake.Exec(db, stmt.Statement()); err != nil {
return fmt.Errorf("error updating security integration: %w", err)
}
}

return ReadSCIMIntegration(d, meta)
}

// DeleteSCIMIntegration implements schema.DeleteFunc
func DeleteSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
return DeleteResource("", snowflake.ScimIntegration)(d, meta)
}
70 changes: 70 additions & 0 deletions pkg/resources/scim_integration_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package resources_test

import (
"fmt"
"os"
"strings"
"testing"

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

func TestAcc_ScimIntegration(t *testing.T) {
if _, ok := os.LookupEnv("SKIP_SCIM_INTEGRATION_TESTS"); ok {
t.Skip("Skipping TestAccScimIntegration")
}

scimIntName := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))
scimIntName2 := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))

resource.Test(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: scimIntegrationConfig_azure(scimIntName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "name", scimIntName),
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "scim_client", "AZURE"),
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "run_as_role", "AAD_PROVISIONER"),
resource.TestCheckResourceAttrSet("snowflake_scim_integration.test_azure_int", "created_on"),
resource.TestCheckResourceAttrSet("snowflake_scim_integration.test_azure_int", "enabled"),
),
},
{
Config: scimIntegrationConfig_azure_np(scimIntName2),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "name", scimIntName2),
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "scim_client", "AZURE"),
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "run_as_role", "AAD_PROVISIONER"),
resource.TestCheckResourceAttr("snowflake_scim_integration.test_azure_int", "network_policy", "AAD_NETWORK_POLICY"),
resource.TestCheckResourceAttrSet("snowflake_scim_integration.test_azure_int", "created_on"),
resource.TestCheckResourceAttrSet("snowflake_scim_integration.test_azure_int", "enabled"),
),
},
},
})
}

func scimIntegrationConfig_azure(name string) string {
return fmt.Sprintf(`
resource "snowflake_scim_integration" "test_azure_int" {
name = "%s"
scim_client = "AZURE"
run_as_role = "AAD_PROVISIONER"
enabled = true
}
`, name)
}

func scimIntegrationConfig_azure_np(name string) string {
return fmt.Sprintf(`
resource "snowflake_scim_integration" "test_azure_int_np" {
name = "%s"
scim_client = "AZURE"
run_as_role = "AAD_PROVISIONER"
network_policy = "AAD_NETWORK_POLICY"
enabled = true
}
`, name)
}