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
49 changes: 49 additions & 0 deletions docs/resources/scim_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
# 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"
provisioner_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.
- **provisioner_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

- **id** (String) The ID of this resource.
- **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.

### 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"
provisioner_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
212 changes: 212 additions & 0 deletions pkg/resources/scim_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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"
"github.com/pkg/errors"
)

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)
},
},
"provisioner_role": {
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.",
},
"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.SetString(`SCIM_CLIENT`, d.Get("scim_client").(string))
stmt.SetString(`RUN_AS_ROLE`, d.Get("provisioner_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 errors.Wrap(err, "error creating security integration")
}

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 errors.Wrap(err, "could not show security integration")
}

// 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
}

// 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 errors.Wrap(err, "could not describe security integration")
}
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 errors.Wrap(err, "unable to parse security integration rows")
}
switch k {
case "NETWORK_POLICY":
if err = d.Set("network_policy", v.(string)); err != nil {
return errors.Wrap(err, "unable to set network policy for security integration")
}
case "RUN_AS_ROLE":
if err = d.Set("provisioner_role", v.(string)); err != nil {
return errors.Wrap(err, "unable to set provisioner role for security integration")
}
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("scim_client") {
runSetStatement = true
stmt.SetString(`SCIM_CLIENT`, d.Get("scim_client").(string))
}

if d.HasChange("provisioner_role") {
runSetStatement = true
stmt.SetString(`RUN_AS_ROLE`, d.Get("provisioner_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 errors.Wrap(err, "error unsetting network_policy")
}
} else {
runSetStatement = true
stmt.SetString(`NETWORK_POLICY`, d.Get("network_policy").(string))
}
}

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

return ReadSCIMIntegration(d, meta)
}

// DeleteSCIMIntegration implements schema.DeleteFunc
func DeleteSCIMIntegration(d *schema.ResourceData, meta interface{}) error {
return DeleteResource("", snowflake.ScimIntegration)(d, meta)
}
83 changes: 83 additions & 0 deletions pkg/resources/scim_integration_acceptance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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))
scimProvisionerRole := "AAD_PROVISIONER"
scimNetworkPolicy := strings.ToUpper(acctest.RandStringFromCharSet(10, acctest.CharSetAlpha))

resource.ParallelTest(t, resource.TestCase{
Providers: providers(),
Steps: []resource.TestStep{
{
Config: scimIntegrationConfig_azure(scimIntName, scimProvisionerRole, scimNetworkPolicy),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_scim_integration.test", "name", scimIntName),
resource.TestCheckResourceAttr("snowflake_scim_integration.test", "scim_client", "AZURE"),
resource.TestCheckResourceAttr("snowflake_scim_integration.test", "provisioner_role", scimProvisionerRole),
resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", scimNetworkPolicy),
resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "created_on"),
),
},
{
ResourceName: "snowflake_scim_integration.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func scimIntegrationConfig_azure(name string, role string, policy string) string {
return fmt.Sprintf(`
resource "snowflake_role" "azure" {
name = "%s"
comment = "test comment"
}

resource "snowflake_account_grant" "azurecua" {
roles = [snowflake_role.azure.name]
privilege = "CREATE USER"
}

resource "snowflake_account_grant" "azurecra" {
roles = [snowflake_role.azure.name]
privilege = "CREATE ROLE"
}

resource "snowflake_role_grants" "azure" {
role_name = snowflake_role.azure.name
roles = ["ACCOUNTADMIN"]
}

resource "snowflake_network_policy" "azure" {
name = "%s"
allowed_ip_list = ["192.168.0.100/24", "29.254.123.20"]
}

resource "snowflake_scim_integration" "test" {
name = "%s"
scim_client = "AZURE"
provisioner_role = snowflake_role.azure.name
network_policy = snowflake_network_policy.azure.name
depends_on = [
snowflake_account_grant.azurecua,
snowflake_account_grant.azurecra,
snowflake_role_grants.azure
]
}
`, role, policy, name)
}