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: Add authentication policy resource #3098

Merged
Merged
Show file tree
Hide file tree
Changes from 15 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
35 changes: 35 additions & 0 deletions docs/resources/account_authentication_policy_attachment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
page_title: "snowflake_account_authentication_policy_attachment Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
Specifies the authentication policy to use for the current account. To set the authentication policy of a different account, use a provider alias.
---

# snowflake_account_authentication_policy_attachment (Resource)

Specifies the authentication policy to use for the current account. To set the authentication policy of a different account, use a provider alias.

## Example Usage

```terraform
resource "snowflake_authentication_policy" "default" {
database = "prod"
schema = "security"
name = "default_policy"
}

resource "snowflake_account_authentication_policy_attachment" "attachment" {
authentication_policy = snowflake_authentication_policy.default.fully_qualified_name
}
```

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

### Required

- `authentication_policy` (String) Qualified name (`"db"."schema"."policy_name"`) of the authentication policy to apply to the current account.

### Read-Only

- `id` (String) The ID of this resource.
35 changes: 35 additions & 0 deletions docs/resources/authentication_policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
page_title: "snowflake_authentication_policy Resource - terraform-provider-snowflake"
subcategory: ""
description: |-

---

# snowflake_authentication_policy (Resource)





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

### Required

- `database` (String) The database in which to create the authentication policy.
- `name` (String) Specifies the identifier for the authentication policy.
- `schema` (String) The schema in which to create the authentication policy.

### Optional

- `authentication_methods` (Set of String) A list of authentication methods that are allowed during login. This parameter accepts one or more of the following values: ALL, SAML, PASSWORD, OAUTH, KEYPAIR.
- `client_types` (Set of String) A list of clients that can authenticate with Snowflake. If a client tries to connect, and the client is not one of the valid CLIENT_TYPES, then the login attempt fails. Allowed values are ALL, SNOWFLAKE_UI, DRIVERS, SNOWSQL. The CLIENT_TYPES property of an authentication policy is a best effort method to block user logins based on specific clients. It should not be used as the sole control to establish a security boundary.
- `comment` (String) Specifies a comment for the authentication policy.
- `mfa_authentication_methods` (Set of String) A list of authentication methods that enforce multi-factor authentication (MFA) during login. Authentication methods not listed in this parameter do not prompt for multi-factor authentication. Allowed values are SAML and PASSWORD.
- `mfa_enrollment` (String) Determines whether a user must enroll in multi-factor authentication. Allowed values are REQUIRED and OPTIONAL. When REQUIRED is specified, Enforces users to enroll in MFA. If this value is used, then the CLIENT_TYPES parameter must include SNOWFLAKE_UI, because Snowsight is the only place users can enroll in multi-factor authentication (MFA).
- `security_integrations` (Set of String) A list of security integrations the authentication policy is associated with. This parameter has no effect when SAML or OAUTH are not in the AUTHENTICATION_METHODS list. All values in the SECURITY_INTEGRATIONS list must be compatible with the values in the AUTHENTICATION_METHODS list. For example, if SECURITY_INTEGRATIONS contains a SAML security integration, and AUTHENTICATION_METHODS contains OAUTH, then you cannot create the authentication policy. To allow all security integrations use ALL as parameter.

### Read-Only

- `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution).
- `id` (String) The ID of this resource.
39 changes: 39 additions & 0 deletions docs/resources/user_authentication_policy_attachment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
page_title: "snowflake_user_authentication_policy_attachment Resource - terraform-provider-snowflake"
subcategory: ""
description: |-
Specifies the authentication policy to use for a certain user.
---

# snowflake_user_authentication_policy_attachment (Resource)

Specifies the authentication policy to use for a certain user.

## Example Usage

```terraform
resource "snowflake_user" "user" {
name = "USER_NAME"
}
resource "snowflake_authentication_policy" "ap" {
database = "prod"
schema = "security"
name = "default_policy"
}

resource "snowflake_user_authentication_policy_attachment" "apa" {
authentication_policy_name = snowflake_authentication_policy.ap.fully_qualified_name
user_name = snowflake_user.user.name
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `authentication_policy_name` (String) Fully qualified name of the authentication policy
- `user_name` (String) User name of the user you want to attach the authentication policy to

### Read-Only

- `id` (String) The ID of this resource.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import snowflake_authentication_policy.example '"<database_name>"."<schema_name>"."<authentication_policy_name>"'
19 changes: 19 additions & 0 deletions examples/resources/snowflake_authentication_policy/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Minimal
resource "snowflake_authentication_policy" "basic" {
database = "database_name"
schema = "schema_name"
name = "network_policy_name"
}

## Complete (with every optional set)
resource "snowflake_authentication_policy" "complete" {
database = "database_name"
schema = "schema_name"
name = "network_policy_name"
authentication_methods = ["ALL"]
mfa_authentication_methods = ["SAML", "PASSWORD"]
mfa_enrollment = "OPTIONAL"
client_types = ["ALL"]
security_integrations = ["ALL"]
Relativity74205 marked this conversation as resolved.
Show resolved Hide resolved
comment = "My authentication policy."
}
2 changes: 1 addition & 1 deletion examples/resources/snowflake_network_policy/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resource "snowflake_network_policy" "basic" {
}

## Complete (with every optional set)
resource "snowflake_network_policy" "basic" {
resource "snowflake_network_policy" "complete" {
name = "network_policy_name"
allowed_network_rule_list = ["<fully qualified network rule id>"]
blocked_network_rule_list = ["<fully qualified network rule id>"]
Expand Down
27 changes: 27 additions & 0 deletions pkg/acceptance/check_destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{
resources.ApiIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.ApiIntegrations.ShowByID)
},
resources.AuthenticationPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.AuthenticationPolicies.ShowByID)
},
resources.CortexSearchService: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error {
return runShowById(ctx, id, client.CortexSearchServices.ShowByID)
},
Expand Down Expand Up @@ -451,6 +454,30 @@ func CheckUserPasswordPolicyAttachmentDestroy(t *testing.T) func(*terraform.Stat
}
}

// CheckUserAuthenticationPolicyAttachmentDestroy is a custom checks that should be later incorporated into generic CheckDestroy
func CheckUserAuthenticationPolicyAttachmentDestroy(t *testing.T) func(*terraform.State) error {
t.Helper()
return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
if rs.Type != "snowflake_user_authentication_policy_attachment" {
continue
}
policyReferences, err := TestClient().PolicyReferences.GetPolicyReferences(t, sdk.NewAccountObjectIdentifierFromFullyQualifiedName(rs.Primary.Attributes["user_name"]), sdk.PolicyEntityDomainUser)
if err != nil {
if strings.Contains(err.Error(), "does not exist or not authorized") {
// Note: this can happen if the Policy Reference or the User has been deleted as well; in this case, ignore the error
continue
}
return err
}
if len(policyReferences) > 0 {
return fmt.Errorf("user authentication policy attachment %v still exists", policyReferences[0].PolicyName)
}
}
return nil
}
}

func TestAccCheckGrantApplicationRoleDestroy(s *terraform.State) error {
client := TestAccProvider.Meta().(*provider.Context).Client
for _, rs := range s.RootModule().Resources {
Expand Down
13 changes: 8 additions & 5 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,15 +421,17 @@ func Provider() *schema.Provider {

func getResources() map[string]*schema.Resource {
return map[string]*schema.Resource{
"snowflake_account": resources.Account(),
"snowflake_account_role": resources.AccountRole(),
"snowflake_account_password_policy_attachment": resources.AccountPasswordPolicyAttachment(),
"snowflake_account_parameter": resources.AccountParameter(),
"snowflake_alert": resources.Alert(),
"snowflake_account": resources.Account(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please format your code with make fmt.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is how it is auto formatted for some reason...

Copy link
Collaborator

@sfc-gh-jmichalak sfc-gh-jmichalak Oct 4, 2024

Choose a reason for hiding this comment

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

That's interesting, but we can fix this on our side. Let's leave this unresolved.

"snowflake_account_authentication_policy_attachment": resources.AccountAuthenticationPolicyAttachment(),
sfc-gh-jmichalak marked this conversation as resolved.
Show resolved Hide resolved
"snowflake_account_role": resources.AccountRole(),
"snowflake_account_password_policy_attachment": resources.AccountPasswordPolicyAttachment(),
"snowflake_account_parameter": resources.AccountParameter(),
"snowflake_alert": resources.Alert(),
"snowflake_api_authentication_integration_with_authorization_code_grant": resources.ApiAuthenticationIntegrationWithAuthorizationCodeGrant(),
"snowflake_api_authentication_integration_with_client_credentials": resources.ApiAuthenticationIntegrationWithClientCredentials(),
"snowflake_api_authentication_integration_with_jwt_bearer": resources.ApiAuthenticationIntegrationWithJwtBearer(),
"snowflake_api_integration": resources.APIIntegration(),
"snowflake_authentication_policy": resources.AuthenticationPolicy(),
"snowflake_cortex_search_service": resources.CortexSearchService(),
"snowflake_database_old": resources.DatabaseOld(),
"snowflake_database": resources.Database(),
Expand Down Expand Up @@ -497,6 +499,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_task": resources.Task(),
"snowflake_unsafe_execute": resources.UnsafeExecute(),
"snowflake_user": resources.User(),
"snowflake_user_authentication_policy_attachment": resources.UserAuthenticationPolicyAttachment(),
"snowflake_user_password_policy_attachment": resources.UserPasswordPolicyAttachment(),
"snowflake_user_public_keys": resources.UserPublicKeys(),
"snowflake_view": resources.View(),
Expand Down
1 change: 1 addition & 0 deletions pkg/provider/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
ApiAuthenticationIntegrationWithClientCredentials resource = "snowflake_api_authentication_integration_with_client_credentials"
ApiAuthenticationIntegrationWithJwtBearer resource = "snowflake_api_authentication_integration_with_jwt_bearer"
ApiIntegration resource = "snowflake_api_integration"
AuthenticationPolicy resource = "snowflake_authentication_policy"
CortexSearchService resource = "snowflake_cortex_search_service"
DatabaseOld resource = "snowflake_database_old"
Database resource = "snowflake_database"
Expand Down
88 changes: 88 additions & 0 deletions pkg/resources/account_authentication_policy_attachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package resources

import (
"context"
"fmt"

"github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider"

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

var accountAuthenticationPolicyAttachmentSchema = map[string]*schema.Schema{
"authentication_policy": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Qualified name (`\"db\".\"schema\".\"policy_name\"`) of the authentication policy to apply to the current account.",
ValidateDiagFunc: IsValidIdentifier[sdk.SchemaObjectIdentifier](),
},
}

// AccountAuthenticationPolicyAttachment returns a pointer to the resource representing an account authentication policy attachment.
func AccountAuthenticationPolicyAttachment() *schema.Resource {
return &schema.Resource{
Description: "Specifies the authentication policy to use for the current account. To set the authentication policy of a different account, use a provider alias.",

Create: CreateAccountAuthenticationPolicyAttachment,
Read: ReadAccountAuthenticationPolicyAttachment,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should also support Update here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you sure, the only parameter authentication_policy has ForceNew enabled and with the other attachment resources it is the same.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, I was not clear: Can we have authentication_policy without ForceNew and handle Update in such a function? I think it should be possible.

Copy link
Contributor Author

@Relativity74205 Relativity74205 Oct 24, 2024

Choose a reason for hiding this comment

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

I think that is a good example to bring up a point. I wouldn't to it at this moment, because it would introduce another inconsistency into the codebase and there are already tons of them, which becomes extremely frustrating for me as someone, which contributes only from time to time. When I run into a problem or am implementing something, I usually look at examples at another place in the codebase. However, for every given thing, there are at least 2 (usually more) options how to do it.
And at least have of the comments in this PR were "we don't do it in this way any more" or something similar, which becomes very frustrating.
Another example I am currently struggling with, is how acceptance_test are done. If I am not mistaken, there are at least three different ways. So which one shall I use, I guess is should be Better tests poc as with the warehouse resource (here I have the problem, that the readme has some errors and the generator makes changes to existing generated files)? Or shall I take another approach?

Regarding your original request: I would do it in another PR and make changes to all attachment resources at the same time for consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hi,

First of all, we really appreciate community contributions. It's great to see users' interest in this project.

The project was started by the community and taken over by Snowflake after a while. So, we expect that there will be some differences in the code. During our work before v1, we came up with a few different ideas, and we want to choose the best one ultimately, but we can't improve every resource at once.

To make contributing more straightforward, we have provided a contribution guide, and if you have any questions regarding this project, don't hesitate to ask us :)

The generator you mentioned is a PoC for speeding up writing tests. It is incomplete and has some limitations, as stated in the generator's README.

Regarding the Update thing, as I stated above, we can handle it on our side.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sfc-gh-jmichalak Sure, I am following the project for some while now while I made some small contributions. And of course, I can understand your position, that you are testing different ideas to find a way to make this project more maintainable in the future and I can perfectly understand why this project is in this state. However, this current state is quite difficult/frustrating for someone like me, which works in this project only 2-3 a year for some weeks and has to understand, what the current best-practice is and where to find correct examples how to implement certain things.
And yes, I have read the contributing guide, however, in this current state it is only partially helpful.

And btw, now from the perspective of the user: Consistent behaviour across similar resources is something what a user wishes a lot. Therefore I wouldn't recommend to add the update behaviour only to the authentication_policy_attachment, but to do it consistent for all attachments.

And thanks for taking over the rest of the work on this resource. :)

Delete: DeleteAccountAuthenticationPolicyAttachment,

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

// CreateAccountAuthenticationPolicyAttachment implements schema.CreateFunc.
func CreateAccountAuthenticationPolicyAttachment(d *schema.ResourceData, meta interface{}) error {
client := meta.(*provider.Context).Client
ctx := context.Background()

authenticationPolicy, ok := sdk.NewObjectIdentifierFromFullyQualifiedName(d.Get("authentication_policy").(string)).(sdk.SchemaObjectIdentifier)
if !ok {
return fmt.Errorf("authentication_policy %s is not a valid authentication policy qualified name, expected format: `\"db\".\"schema\".\"policy\"`", d.Get("authentication_policy"))
}

err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{
Set: &sdk.AccountSet{
AuthenticationPolicy: authenticationPolicy,
},
})
if err != nil {
return err
}

d.SetId(helpers.EncodeSnowflakeID(authenticationPolicy))

return ReadAccountAuthenticationPolicyAttachment(d, meta)
}

func ReadAccountAuthenticationPolicyAttachment(d *schema.ResourceData, meta interface{}) error {
authenticationPolicy := helpers.DecodeSnowflakeID(d.Id())
if err := d.Set("authentication_policy", authenticationPolicy.FullyQualifiedName()); err != nil {
return err
}

return nil
}

// DeleteAccountAuthenticationPolicyAttachment implements schema.DeleteFunc.
func DeleteAccountAuthenticationPolicyAttachment(d *schema.ResourceData, meta interface{}) error {
client := meta.(*provider.Context).Client
ctx := context.Background()

err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{
Unset: &sdk.AccountUnset{
AuthenticationPolicy: sdk.Bool(true),
},
})
if err != nil {
return err
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package resources_test

import (
"fmt"
"testing"

acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestAcc_AccountAuthenticationPolicyAttachment(t *testing.T) {
policyName := acc.TestClient().Ids.Alpha()

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories,
PreCheck: func() { acc.TestAccPreCheck(t) },
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.RequireAbove(tfversion.Version1_5_0),
},
CheckDestroy: nil,
Steps: []resource.TestStep{
{
Config: accountAuthenticationPolicyAttachmentConfig(acc.TestDatabaseName, acc.TestSchemaName, policyName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("snowflake_account_authentication_policy_attachment.att", "id"),
),
},
{
ResourceName: "snowflake_account_authentication_policy_attachment.att",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"initially_suspended",
"wait_for_provisioning",
"query_acceleration_max_scale_factor",
"max_concurrency_level",
"statement_queued_timeout_in_seconds",
"statement_timeout_in_seconds",
Relativity74205 marked this conversation as resolved.
Show resolved Hide resolved
},
},
},
})
}

func accountAuthenticationPolicyAttachmentConfig(databaseName, schemaName, policyName string) string {
s := `
resource "snowflake_authentication_policy" "pa" {
database = "%s"
schema = "%s"
name = "%v"
}

resource "snowflake_account_authentication_policy_attachment" "att" {
authentication_policy = snowflake_authentication_policy.pa.fully_qualified_name
}
`
return fmt.Sprintf(s, databaseName, schemaName, policyName)
}
2 changes: 1 addition & 1 deletion pkg/resources/account_password_policy_attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var accountPasswordPolicyAttachmentSchema = map[string]*schema.Schema{
},
}

// AccountPasswordPolicyAttachment returns a pointer to the resource representing an api integration.
// AccountPasswordPolicyAttachment returns a pointer to the resource representing an account password policy attachment.
func AccountPasswordPolicyAttachment() *schema.Resource {
return &schema.Resource{
Description: "Specifies the password policy to use for the current account. To set the password policy of a different account, use a provider alias.",
Expand Down
Loading