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 snowflake_grant_application_role resource #2690

Merged
merged 22 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e778c23
add application role
sfc-gh-swinkler Apr 10, 2024
b5d4406
create app using tf
sfc-gh-swinkler Apr 11, 2024
f3f9cb6
fix tests
sfc-gh-swinkler Apr 13, 2024
1b44738
Merge branch 'main' into grant-app-role
sfc-gh-swinkler Apr 13, 2024
06e5599
randomize name
sfc-gh-swinkler Apr 13, 2024
a8e5f6f
fix id
sfc-gh-swinkler Apr 16, 2024
40fb643
Merge branch 'main' into grant-app-role
sfc-gh-swinkler Apr 16, 2024
ebcce67
Merge branch 'main' into grant-app-role
sfc-gh-swinkler Apr 18, 2024
76365d5
Merge branch 'main' into grant-app-role
sfc-gh-swinkler Apr 22, 2024
824b56c
Fix linter issues and tests
sfc-gh-jmichalak May 8, 2024
db8d665
Merge remote-tracking branch 'origin/main' into grant-app-role
sfc-gh-jmichalak May 8, 2024
93a1e2f
Restore manifest2
sfc-gh-jmichalak May 8, 2024
10917e5
Fix tests
sfc-gh-jmichalak May 8, 2024
94833d7
Merge remote-tracking branch 'origin/main' into grant-app-role
sfc-gh-jmichalak May 8, 2024
5dc9593
Remove custom provider
sfc-gh-jmichalak May 9, 2024
739686a
Test app creation order
sfc-gh-jmichalak May 9, 2024
8a58368
Merge remote-tracking branch 'origin/main' into grant-app-role
sfc-gh-jmichalak May 15, 2024
7cbd64d
Fixes
sfc-gh-jmichalak May 15, 2024
bc8cf63
Improve acc tests
sfc-gh-jmichalak May 15, 2024
86bb80a
Merge remote-tracking branch 'origin/main' into grant-app-role
sfc-gh-jmichalak May 15, 2024
a978053
Fixes
sfc-gh-jmichalak May 16, 2024
6321eee
Merge remote-tracking branch 'origin/main' into grant-app-role
sfc-gh-jmichalak May 16, 2024
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
66 changes: 66 additions & 0 deletions docs/resources/grant_application_role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
page_title: "snowflake_grant_application_role Resource - terraform-provider-snowflake"
subcategory: ""
description: |-

---

# snowflake_grant_application_role (Resource)



## Example Usage

```terraform
locals {
application_role_identifier = "\"my_appplication\".\"app_role_1\""
}

##################################
### grant application role to account role
##################################


resource "snowflake_role" "role" {
name = "my_role"
}

resource "snowflake_grant_application_role" "g" {
application_role_name = local.application_role_identifier
parent_account_role_name = snowflake_role.role.name
}

##################################
### grant application role to application
##################################

resource "snowflake_grant_application_role" "g" {
application_role_name = local.application_role_identifier
application_name = "my_second_application"
}
```

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

### Required

- `application_role_name` (String) Specifies the identifier for the application role to grant.

### Optional

- `application_name` (String) The fully qualified name of the application on which application role will be granted.
- `parent_account_role_name` (String) The fully qualified name of the account role on which application role will be granted.

### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
# format is application_role_name (string) | object_type (ACCOUNT_ROLE|APPLICATION) | grantee_name (string)
terraform import "\"my_application\".\"app_role_1\"|ACCOUNT_ROLE|\"my_role\""
```
2 changes: 2 additions & 0 deletions examples/resources/snowflake_grant_application_role/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# format is application_role_name (string) | object_type (ACCOUNT_ROLE|APPLICATION) | grantee_name (string)
terraform import "\"my_application\".\"app_role_1\"|ACCOUNT_ROLE|\"my_role\""
26 changes: 26 additions & 0 deletions examples/resources/snowflake_grant_application_role/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
locals {
application_role_identifier = "\"my_appplication\".\"app_role_1\""
}

##################################
### grant application role to account role
##################################


resource "snowflake_role" "role" {
name = "my_role"
}

resource "snowflake_grant_application_role" "g" {
application_role_name = local.application_role_identifier
parent_account_role_name = snowflake_role.role.name
}

##################################
### grant application role to application
##################################

resource "snowflake_grant_application_role" "g" {
application_role_name = local.application_role_identifier
application_name = "my_second_application"
}
1 change: 1 addition & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ func getResources() map[string]*schema.Resource {
"snowflake_file_format": resources.FileFormat(),
"snowflake_function": resources.Function(),
"snowflake_grant_account_role": resources.GrantAccountRole(),
"snowflake_grant_application_role": resources.GrantApplicationRole(),
"snowflake_grant_database_role": resources.GrantDatabaseRole(),
"snowflake_grant_ownership": resources.GrantOwnership(),
"snowflake_grant_privileges_to_role": resources.GrantPrivilegesToRole(),
Expand Down
250 changes: 250 additions & 0 deletions pkg/resources/grant_application_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package resources

import (
"context"
"errors"
"fmt"
"strings"

"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/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var grantApplicationRoleSchema = map[string]*schema.Schema{
"application_role_name": {
Type: schema.TypeString,
Required: true,
Description: "Specifies the identifier for the application role to grant.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.DatabaseObjectIdentifier](),
},
"parent_account_role_name": {
Type: schema.TypeString,
Optional: true,
Description: "The fully qualified name of the account role on which application role will be granted.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
ExactlyOneOf: []string{
"parent_account_role_name",
"application_name",
},
},
"application_name": {
Type: schema.TypeString,
Optional: true,
Description: "The fully qualified name of the application on which application role will be granted.",
ForceNew: true,
ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](),
ExactlyOneOf: []string{
"parent_account_role_name",
"application_name",
},
},
}

func GrantApplicationRole() *schema.Resource {
return &schema.Resource{
CreateContext: CreateContextGrantApplicationRole,
ReadContext: ReadContextGrantApplicationRole,
DeleteContext: DeleteContextGrantApplicationRole,
Schema: grantApplicationRoleSchema,
Importer: &schema.ResourceImporter{
StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
parts := strings.Split(d.Id(), helpers.IDDelimiter)
if len(parts) != 3 {
return nil, fmt.Errorf("invalid ID specified: %v, expected <application_role_name>|<object_type>|<target_identifier>", d.Id())
}
if err := d.Set("application_role_name", parts[0]); err != nil {
return nil, err
}
switch parts[1] {
case "ACCOUNT_ROLE":
if err := d.Set("parent_account_role_name", sdk.NewAccountObjectIdentifier(parts[2]).FullyQualifiedName()); err != nil {
return nil, err
}
case "APPLICATION":
if err := d.Set("application_name", sdk.NewAccountObjectIdentifier(parts[2]).FullyQualifiedName()); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("invalid object type specified: %v, expected ACCOUNT_ROLE, APPLICATION", parts[1])
}

return []*schema.ResourceData{d}, nil
},
},
}
}

func CreateContextGrantApplicationRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*provider.Context).Client
name := d.Get("application_role_name").(string)
applicationRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(name)
// format of snowflakeResourceID is <application_role_identifier>|<object type>|<parent_account_role_name>
var snowflakeResourceID string
if parentRoleName, ok := d.GetOk("parent_account_role_name"); ok && parentRoleName.(string) != "" {
parentRoleIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(parentRoleName.(string))
snowflakeResourceID = strings.Join([]string{applicationRoleIdentifier.FullyQualifiedName(), "ACCOUNT_ROLE", parentRoleIdentifier.FullyQualifiedName()}, helpers.IDDelimiter)
req := sdk.NewGrantApplicationRoleRequest(applicationRoleIdentifier).WithTo(*sdk.NewKindOfRoleRequest().WithRoleName(&parentRoleIdentifier))
if err := client.ApplicationRoles.Grant(ctx, req); err != nil {
return diag.FromErr(err)
}
} else if applicationName, ok := d.GetOk("application_name"); ok && applicationName.(string) != "" {
applicationIdentifier := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(applicationName.(string))
snowflakeResourceID = strings.Join([]string{applicationRoleIdentifier.FullyQualifiedName(), sdk.ObjectTypeApplication.String(), applicationIdentifier.FullyQualifiedName()}, helpers.IDDelimiter)
req := sdk.NewGrantApplicationRoleRequest(applicationRoleIdentifier).WithTo(*sdk.NewKindOfRoleRequest().WithApplicationName(&applicationIdentifier))
if err := client.ApplicationRoles.Grant(ctx, req); err != nil {
return diag.FromErr(err)
}
}
d.SetId(snowflakeResourceID)
return ReadContextGrantApplicationRole(ctx, d, meta)
}

func ReadContextGrantApplicationRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*provider.Context).Client
parts := strings.Split(d.Id(), helpers.IDDelimiter)
applicationRoleName := parts[0]
applicationRoleIdentifier := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(applicationRoleName)
objectTypeString := parts[1]
if objectTypeString == "ACCOUNT_ROLE" {
objectTypeString = "ROLE"
}

// first check if either the target account role or application exists
targetIdentifier := parts[2]
objectType := sdk.ObjectType(objectTypeString)
switch objectType {
case sdk.ObjectTypeRole:
{
if _, err := client.Roles.ShowByID(ctx, sdk.NewAccountObjectIdentifierFromFullyQualifiedName(targetIdentifier)); err != nil && errors.Is(err, sdk.ErrObjectNotFound) {
d.SetId("")
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Warning,
Summary: "Failed to retrieve account role. Marking the resource as removed.",
Detail: fmt.Sprintf("Id: %s", d.Id()),
},
}
}
}
case sdk.ObjectTypeApplication:
if _, err := client.Applications.ShowByID(ctx, sdk.NewAccountObjectIdentifierFromFullyQualifiedName(targetIdentifier)); err != nil && errors.Is(err, sdk.ErrObjectNotFound) {
d.SetId("")
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Warning,
Summary: "Failed to retrieve application. Marking the resource as removed.",
Detail: fmt.Sprintf("Id: %s", d.Id()),
},
}
}
}
// then check if application role exists
grants, err := client.Grants.Show(ctx, &sdk.ShowGrantOptions{
Of: &sdk.ShowGrantsOf{
ApplicationRole: applicationRoleIdentifier,
},
})
if err != nil {
sfc-gh-jcieslak marked this conversation as resolved.
Show resolved Hide resolved
if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) {
d.SetId("")
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Warning,
Summary: "Failed to retrieve application role. Marking the resource as removed.",
Detail: fmt.Sprintf("Id: %s", d.Id()),
},
}
} else {
return diag.FromErr(err)
}
}
sfc-gh-jcieslak marked this conversation as resolved.
Show resolved Hide resolved

// finally check if the grant of the application role to the target (account role / application) exists
var found bool
for _, grant := range grants {
if grant.GrantedTo == objectType {
if grant.GrantedTo == sdk.ObjectTypeRole || grant.GrantedTo == sdk.ObjectTypeApplication {
if grant.GranteeName.FullyQualifiedName() == targetIdentifier {
found = true
break
}
} else {
/*
note that grantee_name is not saved as a valid identifier in the
SHOW GRANTS OF APPLICATION ROLE <application_role_name> command
for example, "ABC"."test_parent_role" is saved as ABC."test_parent_role"
or "ABC"."test_parent_role" is saved as ABC.test_parent_role
and our internal mapper thereby fails to parse it correctly, returning "ABC."test_parent_role"
so this funny string replacement is needed to make it work
*/
s := grant.GranteeName.FullyQualifiedName()
if !strings.Contains(s, "\"") {
parts := strings.Split(s, ".")
s = sdk.NewDatabaseObjectIdentifier(parts[0], parts[1]).FullyQualifiedName()
} else {
parts := strings.Split(s, "\".\"")
if len(parts) < 2 {
parts = strings.Split(s, "\".")
if len(parts) < 2 {
parts = strings.Split(s, ".\"")
if len(parts) < 2 {
s = strings.Trim(s, "\"")
parts = strings.Split(s, ".")
}
}
}
s = sdk.NewDatabaseObjectIdentifier(parts[0], parts[1]).FullyQualifiedName()
}
if s == targetIdentifier {
found = true
break
}
}
}
}
if !found {
d.SetId("")
return diag.Diagnostics{
diag.Diagnostic{
Severity: diag.Warning,
Summary: "Application role grant not found. Marking the resource as removed.",
Detail: fmt.Sprintf("Id: %s", d.Id()),
},
}
}

return nil
}

func DeleteContextGrantApplicationRole(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*provider.Context).Client

parts := strings.Split(d.Id(), "|")
id := sdk.NewDatabaseObjectIdentifierFromFullyQualifiedName(parts[0])
objectType := parts[1]
granteeName := parts[2]
switch objectType {
case "ACCOUNT_ROLE":
applicationRoleName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName)
if err := client.ApplicationRoles.Revoke(ctx, sdk.NewRevokeApplicationRoleRequest(id).WithFrom(*sdk.NewKindOfRoleRequest().WithRoleName(&applicationRoleName))); err != nil {
if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) {
return diag.FromErr(err)
}
}
case "APPLICATION":
applicationName := sdk.NewAccountObjectIdentifierFromFullyQualifiedName(granteeName)
if err := client.ApplicationRoles.Revoke(ctx, sdk.NewRevokeApplicationRoleRequest(id).WithFrom(*sdk.NewKindOfRoleRequest().WithApplicationName(&applicationName))); err != nil {
if errors.Is(err, sdk.ErrObjectNotExistOrAuthorized) {
return diag.FromErr(err)
}
}
}
d.SetId("")
return nil
}
Loading
Loading