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

feat: OAuth security integration for partner applications #763

Merged
merged 19 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/resources/oauth_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "snowflake_oauth_integration Resource - terraform-provider-snowflake"
subcategory: ""
description: |-

---

# snowflake_oauth_integration (Resource)



## Example Usage

```terraform
resource "snowflake_oauth_integration" "tableau_desktop" {
name = "TABLEAU_DESKTOP"
oauth_client = "TABLEAU_DESKTOP"
enabled = true
oauth_issue_refresh_tokens = true
oauth_refresh_token_validity = 3600
blocked_roles_list = ["SYSADMIN"]
}
```

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

### Required

- **name** (String) Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.
- **oauth_client** (String) Specifies the OAuth client type.

### Optional

- **blocked_roles_list** (Set of String) List of roles that a user cannot explicitly consent to using after authenticating. Do not include ACCOUNTADMIN, ORGADMIN or SECURITYADMIN as they are already implicitly enforced and will cause in-place updates.
- **comment** (String) Specifies a comment for the OAuth integration.
- **enabled** (Boolean) Specifies whether this OAuth integration is enabled or disabled.
- **id** (String) The ID of this resource.
- **oauth_issue_refresh_tokens** (Boolean) Specifies whether to allow the client to exchange a refresh token for an access token when the current access token has expired.
- **oauth_refresh_token_validity** (Number) Specifies how long refresh tokens should be valid (in seconds). OAUTH_ISSUE_REFRESH_TOKENS must be set to TRUE.
- **oauth_use_secondary_roles** (String) Specifies whether default secondary roles set in the user properties are activated by default in the session being opened.

### Read-Only

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

## Import

Import is supported using the following syntax:

```shell
terraform import snowflake_oauth_integration.example name
```
1 change: 1 addition & 0 deletions examples/resources/snowflake_oauth_integration/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import snowflake_oauth_integration.example name
8 changes: 8 additions & 0 deletions examples/resources/snowflake_oauth_integration/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "snowflake_oauth_integration" "tableau_desktop" {
name = "TABLEAU_DESKTOP"
oauth_client = "TABLEAU_DESKTOP"
enabled = true
oauth_issue_refresh_tokens = true
oauth_refresh_token_validity = 3600
blocked_roles_list = ["SYSADMIN"]
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_materialized_view": resources.MaterializedView(),
"snowflake_network_policy_attachment": resources.NetworkPolicyAttachment(),
"snowflake_network_policy": resources.NetworkPolicy(),
"snowflake_oauth_integration": resources.OAuthIntegration(),
"snowflake_pipe": resources.Pipe(),
"snowflake_procedure": resources.Procedure(),
"snowflake_resource_monitor": resources.ResourceMonitor(),
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 @@ -208,6 +208,14 @@ func scimIntegration(t *testing.T, id string, params map[string]interface{}) *sc
return d
}

func oauthIntegration(t *testing.T, id string, params map[string]interface{}) *schema.ResourceData {
r := require.New(t)
d := schema.TestResourceDataRaw(t, resources.OAuthIntegration().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
302 changes: 302 additions & 0 deletions pkg/resources/oauth_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
package resources

import (
"database/sql"
"fmt"
"log"
"strconv"
"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 oauthIntegrationSchema = map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Specifies the name of the OAuth integration. This name follows the rules for Object Identifiers. The name should be unique among security integrations in your account.",
},
"oauth_client": {
Type: schema.TypeString,
Required: true,
Description: "Specifies the OAuth client type.",
ValidateFunc: validation.StringInSlice([]string{
"TABLEAU_DESKTOP", "TABLEAU_SERVER", "LOOKER",
}, false),
},
"oauth_issue_refresh_tokens": {
Type: schema.TypeBool,
Optional: true,
Description: "Specifies whether to allow the client to exchange a refresh token for an access token when the current access token has expired.",
},
"oauth_refresh_token_validity": {
Type: schema.TypeInt,
Optional: true,
Description: "Specifies how long refresh tokens should be valid (in seconds). OAUTH_ISSUE_REFRESH_TOKENS must be set to TRUE.",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need oauth_issue_refresh_tokens or would the presence/absence of oauth_refresh_token_validity give us enough information?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that keeping both would more closely match Snowflake's api, so we might want to keep as is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like oauth_issue_refresh_tokens is true by default, so you need it here if you want to set it to false. However, oauth_refresh_token_validity is optional too, it defaults to various values depending on the client (10 hours for Tableau Desktop, 90 days for Tableau Server, etc) - there will be cases where people omit both or specify only one.

},
"oauth_use_secondary_roles": {
Type: schema.TypeString,
Optional: true,
Default: "NONE",
Description: "Specifies whether default secondary roles set in the user properties are activated by default in the session being opened.",
ValidateFunc: validation.StringInSlice([]string{
"IMPLICIT", "NONE",
}, false),
},
"blocked_roles_list": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Description: "List of roles that a user cannot explicitly consent to using after authenticating. Do not include ACCOUNTADMIN, ORGADMIN or SECURITYADMIN as they are already implicitly enforced and will cause in-place updates.",
},
"comment": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies a comment for the OAuth integration.",
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Description: "Specifies whether this OAuth integration is enabled or disabled.",
},
"created_on": {
Type: schema.TypeString,
Computed: true,
Description: "Date and time when the OAuth integration was created.",
},
}

// OAuthIntegration returns a pointer to the resource representing an OAuth integration
func OAuthIntegration() *schema.Resource {
return &schema.Resource{
Create: CreateOAuthIntegration,
Read: ReadOAuthIntegration,
Update: UpdateOAuthIntegration,
Delete: DeleteOAuthIntegration,

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

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

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

// Set required fields
stmt.SetRaw(`TYPE=OAUTH`)
stmt.SetString(`OAUTH_CLIENT`, d.Get("oauth_client").(string))

// Set optional fields
if _, ok := d.GetOk("oauth_issue_refresh_tokens"); ok {
stmt.SetBool(`OAUTH_ISSUE_REFRESH_TOKENS`, d.Get("oauth_issue_refresh_tokens").(bool))
}
if _, ok := d.GetOk("oauth_refresh_token_validity"); ok {
stmt.SetInt(`OAUTH_REFRESH_TOKEN_VALIDITY`, d.Get("oauth_refresh_token_validity").(int))
}
if _, ok := d.GetOk("oauth_use_secondary_roles"); ok {
stmt.SetString(`OAUTH_USE_SECONDARY_ROLES`, d.Get("oauth_use_secondary_roles").(string))
}
if _, ok := d.GetOk("blocked_roles_list"); ok {
stmt.SetStringList(`BLOCKED_ROLES_LIST`, expandStringList(d.Get("blocked_roles_list").(*schema.Set).List()))
}
if _, ok := d.GetOk("enabled"); ok {
stmt.SetBool(`ENABLED`, d.Get("enabled").(bool))
}
if _, ok := d.GetOk("comment"); ok {
stmt.SetString(`COMMENT`, d.Get("comment").(string))
}

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

d.SetId(name)

return ReadOAuthIntegration(d, meta)
}

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

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

// Some properties can come from the SHOW INTEGRATION call

s, err := snowflake.ScanOAuthIntegration(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("oauth_client", strings.TrimPrefix(s.IntegrationType.String, "OAUTH - ")); err != nil {
return err
}

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

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

if err := d.Set("comment", s.Comment.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.OAuthIntegration(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 {
return errors.Wrap(err, "unable to parse security integration rows")
}
switch k {
case "ENABLED":
// We set this using the SHOW INTEGRATION call so let's ignore it here
case "COMMENT":
// We set this using the SHOW INTEGRATION call so let's ignore it here
case "OAUTH_ISSUE_REFRESH_TOKENS":
b, err := strconv.ParseBool(v.(string))
if err != nil {
return errors.Wrap(err, "returned OAuth issue refresh tokens that is not boolean")
}
if err = d.Set("oauth_issue_refresh_tokens", b); err != nil {
return errors.Wrap(err, "unable to set OAuth issue refresh tokens for security integration")
}
case "OAUTH_REFRESH_TOKEN_VALIDITY":
i, err := strconv.Atoi(v.(string))
if err != nil {
return errors.Wrap(err, "returned OAuth refresh token validity that is not integer")
}
if err = d.Set("oauth_refresh_token_validity", i); err != nil {
return errors.Wrap(err, "unable to set OAuth refresh token validity for security integration")
}
case "OAUTH_USE_SECONDARY_ROLES":
if err = d.Set("oauth_use_secondary_roles", v.(string)); err != nil {
return errors.Wrap(err, "unable to set OAuth use secondary roles for security integration")
}
case "BLOCKED_ROLES_LIST":
blockedRolesAll := strings.Split(v.(string), ",")

// Only roles other than ACCOUNTADMIN, ORGADMIN and SECURITYADMIN can be specified custom,
// those three are enforced with no option to remove them
blockedRolesCustom := []string{}
for _, role := range blockedRolesAll {
if role != "ACCOUNTADMIN" && role != "ORGADMIN" && role != "SECURITYADMIN" {
blockedRolesCustom = append(blockedRolesCustom, role)
}
}

if err = d.Set("blocked_roles_list", blockedRolesCustom); err != nil {
return errors.Wrap(err, "unable to set blocked roles list for security integration")
}
case "OAUTH_CLIENT_TYPE":
// Only used for custom OAuth clients (not supported yet)
case "OAUTH_ENFORCE_PKCE":
// Only used for custom OAuth clients (not supported yet)
case "OAUTH_AUTHORIZATION_ENDPOINT":
// Only used for custom OAuth clients (not supported yet)
case "OAUTH_TOKEN_ENDPOINT":
// Only used for custom OAuth clients (not supported yet)
case "OAUTH_ALLOWED_AUTHORIZATION_ENDPOINTS":
// Only used for custom OAuth clients (not supported yet)
case "OAUTH_ALLOWED_TOKEN_ENDPOINTS":
// Only used for custom OAuth clients (not supported yet)
case "PRE_AUTHORIZED_ROLES_LIST":
// Only used for custom OAuth clients (not supported yet)

default:
log.Printf("[WARN] unexpected security integration property %v returned from Snowflake", k)
}
}

return err
}

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

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

var runSetStatement bool

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

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

if d.HasChange("oauth_refresh_token_validity") {
runSetStatement = true
stmt.SetInt(`OAUTH_REFRESH_TOKEN_VALIDITY`, d.Get("oauth_refresh_token_validity").(int))
}

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

if d.HasChange("blocked_roles_list") {
runSetStatement = true
stmt.SetStringList(`BLOCKED_ROLES_LIST`, expandStringList(d.Get("blocked_roles_list").([]interface{})))
}

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

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

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

return ReadOAuthIntegration(d, meta)
}

// DeleteOAuthIntegration implements schema.DeleteFunc
func DeleteOAuthIntegration(d *schema.ResourceData, meta interface{}) error {
return DeleteResource("", snowflake.OAuthIntegration)(d, meta)
}
Loading