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: SAML2 integration v1 readiness #2868

Merged
merged 20 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
7 changes: 3 additions & 4 deletions docs/resources/saml2_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,10 @@ resource "snowflake_saml2_integration" "test" {
- `saml2_enable_sp_initiated` (String) The Boolean indicating if the Log In With button will be shown on the login page. TRUE: displays the Log in With button on the login page. FALSE: does not display the Log in With button on the login page. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.
- `saml2_force_authn` (String) The Boolean indicating whether users, during the initial authentication flow, are forced to authenticate again to access Snowflake. When set to TRUE, Snowflake sets the ForceAuthn SAML parameter to TRUE in the outgoing request from Snowflake to the identity provider. TRUE: forces users to authenticate again to access Snowflake, even if a valid session with the identity provider exists. FALSE: does not force users to authenticate again to access Snowflake. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.
- `saml2_post_logout_redirect_url` (String) The endpoint to which Snowflake redirects users after clicking the Log Out button in the classic Snowflake web interface. Snowflake terminates the Snowflake session upon redirecting to the specified endpoint.
- `saml2_requested_nameid_format` (String) The SAML NameID format allows Snowflake to set an expectation of the identifying attribute of the user (i.e. SAML Subject) in the SAML assertion from the IdP to ensure a valid authentication to Snowflake. Valid options are: [urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient] When the value is not set in the configuration the provider will put `Snowflake default value` in the state which is a placeholder that means to use the Snowflake default for this value.
- `saml2_requested_nameid_format` (String) The SAML NameID format allows Snowflake to set an expectation of the identifying attribute of the user (i.e. SAML Subject) in the SAML assertion from the IdP to ensure a valid authentication to Snowflake. Valid options are: [urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient]
- `saml2_sign_request` (String) The Boolean indicating whether SAML requests are signed. TRUE: allows SAML requests to be signed. FALSE: does not allow SAML requests to be signed. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.
- `saml2_snowflake_acs_url` (String) The string containing the Snowflake Assertion Consumer Service URL to which the IdP will send its SAML authentication response back to Snowflake. This property will be set in the SAML authentication request generated by Snowflake when initiating a SAML SSO operation with the IdP. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use. When the value is not set in the configuration the provider will put `Snowflake default value` in the state which is a placeholder that means to use the Snowflake default for this value.
- `saml2_snowflake_issuer_url` (String) The string containing the EntityID / Issuer for the Snowflake service provider. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use. When the value is not set in the configuration the provider will put `Snowflake default value` in the state which is a placeholder that means to use the Snowflake default for this value.
- `saml2_snowflake_x509_cert` (String) The Base64 encoded self-signed certificate generated by Snowflake for use with Encrypting SAML Assertions and Signed SAML Requests. You must have at least one of these features (encrypted SAML assertions or signed SAML responses) enabled in your Snowflake account to access the certificate value. When the value is not set in the configuration the provider will put `Snowflake default value` in the state which is a placeholder that means to use the Snowflake default for this value.
- `saml2_snowflake_acs_url` (String) The string containing the Snowflake Assertion Consumer Service URL to which the IdP will send its SAML authentication response back to Snowflake. This property will be set in the SAML authentication request generated by Snowflake when initiating a SAML SSO operation with the IdP. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use.
- `saml2_snowflake_issuer_url` (String) The string containing the EntityID / Issuer for the Snowflake service provider. If an incorrect value is specified, Snowflake returns an error message indicating the acceptable values to use.
- `saml2_sp_initiated_login_page_label` (String) The string containing the label to display after the Log In With button on the login page. If this field changes value from non-empty to empty, the whole resource is recreated because of Snowflake limitations.

### Read-Only
Expand Down
9 changes: 7 additions & 2 deletions pkg/acceptance/helpers/security_integration_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,19 @@ func (c *SecurityIntegrationClient) CreateScim(t *testing.T) (*sdk.SecurityInteg
return c.CreateScimWithRequest(t, sdk.NewCreateScimSecurityIntegrationRequest(c.ids.RandomAccountObjectIdentifier(), sdk.ScimSecurityIntegrationScimClientGeneric, sdk.ScimSecurityIntegrationRunAsRoleGenericScimProvisioner))
}

func (c *SecurityIntegrationClient) UpdateSaml2ForceAuthn(t *testing.T, id sdk.AccountObjectIdentifier, forceAuthn bool) {
func (c *SecurityIntegrationClient) UpdateSaml2(t *testing.T, request *sdk.AlterSaml2SecurityIntegrationRequest) {
t.Helper()
ctx := context.Background()

err := c.client().AlterSaml2(ctx, sdk.NewAlterSaml2SecurityIntegrationRequest(id).WithSet(*sdk.NewSaml2IntegrationSetRequest().WithSaml2ForceAuthn(forceAuthn)))
err := c.client().AlterSaml2(ctx, request)
require.NoError(t, err)
}

func (c *SecurityIntegrationClient) UpdateSaml2ForceAuthn(t *testing.T, id sdk.AccountObjectIdentifier, forceAuthn bool) {
t.Helper()
c.UpdateSaml2(t, sdk.NewAlterSaml2SecurityIntegrationRequest(id).WithSet(*sdk.NewSaml2IntegrationSetRequest().WithSaml2ForceAuthn(forceAuthn)))
}

func (c *SecurityIntegrationClient) CreateScimWithRequest(t *testing.T, request *sdk.CreateScimSecurityIntegrationRequest) (*sdk.SecurityIntegration, func()) {
t.Helper()
ctx := context.Background()
Expand Down
8 changes: 0 additions & 8 deletions pkg/resources/common.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
package resources

import (
"fmt"
"strings"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// TODO: move to special values
const SnowflakeDefaultStringValuePlaceholder = "Snowflake default value"

func SnowflakeDefaultStringValueDescription(description string) string {
return fmt.Sprintf("%s When the value is not set in the configuration the provider will put `%s` in the state which is a placeholder that means to use the Snowflake default for this value.", description, SnowflakeDefaultStringValuePlaceholder)
}

// DiffSuppressStatement will suppress diffs between statements if they differ in only case or in
// runs of whitespace (\s+ = \s). This is needed because the snowflake api does not faithfully
// round-trip queries, so we cannot do a simple character-wise comparison to detect changes.
Expand Down
6 changes: 3 additions & 3 deletions pkg/resources/custom_diffs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ func TestForceNewIfChangeToEmptySet(t *testing.T) {
}, {
name: "non-empty to empty",
stateValue: map[string]string{
"value.#": "1",
"value.0": "foo",
"value.#": "1",
Copy link
Collaborator

Choose a reason for hiding this comment

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

please add some comment to explain this funny stuff (in the next PR)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Addressed in #2912

"value.2577344683": "CREATE DATABASE",
},
rawConfigValue: map[string]any{},
wantForceNew: true,
Expand All @@ -324,7 +324,7 @@ func TestForceNewIfChangeToEmptySet(t *testing.T) {
diff := calculateDiffFromAttributes(t,
createProviderWithValuePropertyAndCustomDiff(t,
&schema.Schema{
Type: schema.TypeList,
Type: schema.TypeSet,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Expand Down
78 changes: 60 additions & 18 deletions pkg/resources/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,25 +141,67 @@ func GetPropertyAsPointer[T any](d *schema.ResourceData, property string) *T {
return &typedValue
}

// ParseCommaSeparatedStringArray can be used to parse Snowflake output containing a list in the format of "[item1, item2, ...]",
// the assumptions are that:
// 1. The list is enclosed by [] brackets, and they shouldn't be a part of any item's value
// 2. Items are separated by commas, and they shouldn't be a part of any item's value
// 3. Items can have as many spaces in between, but after separation they will be trimmed and shouldn't be a part of any item's value
func ParseCommaSeparatedStringArray(value string) []string {
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
if value == "[]" {
return make([]string, 0)
}
list := strings.Trim(value, "[]")
listItems := strings.Split(list, ",")
trimmedListItems := make([]string, len(listItems))
for i, item := range listItems {
trimmedListItems[i] = strings.TrimSpace(item)
}
return trimmedListItems
func GetConfigPropertyAsPointerAllowingZeroValue[T any](d *schema.ResourceData, property string) *T {
if d.GetRawConfig().AsValueMap()[property].IsNull() {
return nil
}
value := d.Get(property)
typedValue, ok := value.(T)
if !ok {
return nil
}
return &typedValue
}

func GetPropertyOfFirstNestedObjectByValueKey[T any](d *schema.ResourceData, propertyKey string) (*T, error) {
return GetPropertyOfFirstNestedObjectByKey[T](d, propertyKey, "value")
}

// GetPropertyOfFirstNestedObjectByKey should be used for single objects defined in the Terraform schema as
// schema.TypeList with MaxItems set to one and inner schema with single value. To easily retrieve
// the inner value, you can specify the top-level property with propertyKey and the nested value with nestedValueKey.
func GetPropertyOfFirstNestedObjectByKey[T any](d *schema.ResourceData, propertyKey string, nestedValueKey string) (*T, error) {
value, ok := d.GetOk(propertyKey)
if !ok {
return nil, fmt.Errorf("nested property %s not found", propertyKey)
}

typedValue, ok := value.([]any)
if !ok || len(typedValue) != 1 {
return nil, fmt.Errorf("nested property %s is not an array or has incorrect number of values: %d, expected: 1", propertyKey, len(typedValue))
}

typedNestedMap, ok := typedValue[0].(map[string]any)
if !ok {
return nil, fmt.Errorf("nested property %s is not of type map[string]any, got: %T", propertyKey, typedValue[0])
}
return make([]string, 0)

_, ok = typedNestedMap[nestedValueKey]
if !ok {
return nil, fmt.Errorf("nested value key %s couldn't be found in the nested property map %s", nestedValueKey, propertyKey)
}

typedNestedValue, ok := typedNestedMap[nestedValueKey].(T)
if !ok {
return nil, fmt.Errorf("nested property %s.%s is not of type %T, got: %T", propertyKey, nestedValueKey, *new(T), typedNestedMap[nestedValueKey])
}

return &typedNestedValue, nil
}

func SetPropertyOfFirstNestedObjectByValueKey[T any](d *schema.ResourceData, propertyKey string, value T) error {
return SetPropertyOfFirstNestedObjectByKey[T](d, propertyKey, "value", value)
}

// SetPropertyOfFirstNestedObjectByKey should be used for single objects defined in the Terraform schema as
// schema.TypeList with MaxItems set to one and inner schema with single value. To easily set
// the inner value, you can specify top-level property with propertyKey, nested value with nestedValueKey and value at the end.
func SetPropertyOfFirstNestedObjectByKey[T any](d *schema.ResourceData, propertyKey string, nestedValueKey string, value T) error {
return d.Set(propertyKey, []any{
map[string]any{
nestedValueKey: value,
},
})
}

type tags []tag
Expand Down
50 changes: 0 additions & 50 deletions pkg/resources/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,53 +258,3 @@ func TestListDiff(t *testing.T) {
})
}
}

func TestParseCommaSeparatedStringArray(t *testing.T) {
testCases := []struct {
Name string
Value string
Result []string
}{
{
Name: "empty list",
Value: "[]",
Result: []string{},
},
{
Name: "empty string",
Value: "",
Result: []string{},
},
{
Name: "one element in list",
Value: "[one]",
Result: []string{"one"},
},
{
Name: "multiple elements in list",
Value: "[one, two, three]",
Result: []string{"one", "two", "three"},
},
{
Name: "multiple elements in list - packed",
Value: "[one,two,three]",
Result: []string{"one", "two", "three"},
},
{
Name: "multiple elements in list - additional spaces",
Value: "[one , two ,three]",
Result: []string{"one", "two", "three"},
},
{
Name: "list without brackets",
Value: "one,two,three",
Result: []string{},
},
}

for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
assert.Equal(t, tc.Result, resources.ParseCommaSeparatedStringArray(tc.Value))
})
}
}
Loading
Loading