diff --git a/.changelog/19817.txt b/.changelog/19817.txt new file mode 100644 index 00000000000..4ef139133bc --- /dev/null +++ b/.changelog/19817.txt @@ -0,0 +1,15 @@ +```release-note:bug +resource/aws_lakeformation_permissions: Fix bug preventing updates (inconsistent result) +``` + +```release-note:bug +resource/aws_lakeformation_permissions: Fix bug where resource is not properly removed from state +``` + +```release-note:bug +resource/aws_lakeformation_permissions: Fix diffs resulting only from order of column names and exclude column names +``` + +```release-note:bug +data-source/aws_lakeformation_permissions: Fix diffs resulting from order of column names and exclude column names +``` \ No newline at end of file diff --git a/aws/data_source_aws_lakeformation_permissions.go b/aws/data_source_aws_lakeformation_permissions.go index 2529d491d12..9a9c48e777c 100644 --- a/aws/data_source_aws_lakeformation_permissions.go +++ b/aws/data_source_aws_lakeformation_permissions.go @@ -6,13 +6,11 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lakeformation" - "github.com/hashicorp/aws-sdk-go-base/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" - iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" - "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" + tflakeformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation/waiter" ) func dataSourceAwsLakeFormationPermissions() *schema.Resource { @@ -134,8 +132,9 @@ func dataSourceAwsLakeFormationPermissions() *schema.Resource { ValidateFunc: validateAwsAccountId, }, "column_names": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.NoZeroValues, @@ -146,8 +145,9 @@ func dataSourceAwsLakeFormationPermissions() *schema.Resource { Required: true, }, "excluded_column_names": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.NoZeroValues, @@ -198,89 +198,49 @@ func dataSourceAwsLakeFormationPermissionsRead(d *schema.ResourceData, meta inte if v, ok := d.GetOk("table"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { input.Resource.Table = expandLakeFormationTableResource(v.([]interface{})[0].(map[string]interface{})) - tableType = TableTypeTable + tableType = tflakeformation.TableTypeTable } if v, ok := d.GetOk("table_with_columns"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { // can't ListPermissions for TableWithColumns, so use Table instead input.Resource.Table = expandLakeFormationTableWithColumnsResourceAsTable(v.([]interface{})[0].(map[string]interface{})) - tableType = TableTypeTableWithColumns + tableType = tflakeformation.TableTypeTableWithColumns } - log.Printf("[DEBUG] Reading Lake Formation permissions: %v", input) - var allPermissions []*lakeformation.PrincipalResourcePermissions - - err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { - err := conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { - for _, permission := range resp.PrincipalResourcePermissions { - if permission == nil { - continue - } + columnNames := make([]*string, 0) + excludedColumnNames := make([]*string, 0) + columnWildcard := false - allPermissions = append(allPermissions, permission) - } - return !lastPage - }) + if tableType == tflakeformation.TableTypeTableWithColumns { + if v, ok := d.GetOk("table_with_columns.0.wildcard"); ok { + columnWildcard = v.(bool) + } - if err != nil { - if tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "Invalid principal") { - return resource.RetryableError(err) + if v, ok := d.GetOk("table_with_columns.0.column_names"); ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + columnNames = expandStringSet(v) } - return resource.NonRetryableError(fmt.Errorf("error reading Lake Formation Permissions: %w", err)) } - return nil - }) - - if tfresource.TimedOut(err) { - err = conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { - for _, permission := range resp.PrincipalResourcePermissions { - if permission == nil { - continue - } - allPermissions = append(allPermissions, permission) + if v, ok := d.GetOk("table_with_columns.0.excluded_column_names"); ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + excludedColumnNames = expandStringSet(v) } - return !lastPage - }) + } } + log.Printf("[DEBUG] Reading Lake Formation permissions: %v", input) + + allPermissions, err := waiter.PermissionsReady(conn, input, tableType, columnNames, excludedColumnNames, columnWildcard) + d.SetId(fmt.Sprintf("%d", hashcode.String(input.String()))) if err != nil { return fmt.Errorf("error reading Lake Formation permissions: %w", err) } - var cleanPermissions []*lakeformation.PrincipalResourcePermissions - - if input.Resource.Catalog != nil { - cleanPermissions = filterLakeFormationCatalogPermissions(allPermissions) - } - - if input.Resource.DataLocation != nil { - cleanPermissions = filterLakeFormationDataLocationPermissions(allPermissions) - } - - if input.Resource.Database != nil { - cleanPermissions = filterLakeFormationDatabasePermissions(allPermissions) - } - - if tableType == TableTypeTable { - cleanPermissions = filterLakeFormationTablePermissions( - aws.StringValue(input.Resource.Table.Name), - input.Resource.Table.TableWildcard != nil, - allPermissions, - ) - } - - if tableType == TableTypeTableWithColumns { - cleanPermissions = filterLakeFormationTableWithColumnsPermissions( - d.Get("table_with_columns.0.database_name").(string), - d.Get("table_with_columns.0.wildcard").(bool), - expandStringList(d.Get("table_with_columns.0.column_names").([]interface{})), - expandStringList(d.Get("table_with_columns.0.excluded_column_names").([]interface{})), - allPermissions, - ) - } + // clean permissions = filter out permissions that do not pertain to this specific resource + cleanPermissions := tflakeformation.FilterPermissions(input, tableType, columnNames, excludedColumnNames, columnWildcard, allPermissions) if len(cleanPermissions) != len(allPermissions) { log.Printf("[INFO] Resource Lake Formation clean permissions (%d) and all permissions (%d) have different lengths (this is not necessarily a problem): %s", len(cleanPermissions), len(allPermissions), d.Id()) @@ -292,6 +252,8 @@ func dataSourceAwsLakeFormationPermissionsRead(d *schema.ResourceData, meta inte if cleanPermissions[0].Resource.Catalog != nil { d.Set("catalog_resource", true) + } else { + d.Set("catalog_resource", false) } if cleanPermissions[0].Resource.DataLocation != nil { diff --git a/aws/internal/service/lakeformation/filter.go b/aws/internal/service/lakeformation/filter.go new file mode 100644 index 00000000000..916df407ebd --- /dev/null +++ b/aws/internal/service/lakeformation/filter.go @@ -0,0 +1,181 @@ +package lakeformation + +import ( + "reflect" + "sort" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lakeformation" +) + +const ( + TableNameAllTables = "ALL_TABLES" + TableTypeTable = "Table" + TableTypeTableWithColumns = "TableWithColumns" +) + +func FilterPermissions(input *lakeformation.ListPermissionsInput, tableType string, columnNames []*string, excludedColumnNames []*string, columnWildcard bool, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + // For most Lake Formation resources, filtering within the provider is unnecessary. The input + // contains everything for AWS to give you back exactly what you want. However, many challenges + // arise with tables and tables with columns. Perhaps the two biggest problems (so far) are as + // follows: + // 1. SELECT - when you grant SELECT, it may be part of a list of permissions. However, when + // listing permissions, SELECT comes back in a separate permission. + // 2. Tables with columns. The ListPermissionsInput does not allow you to include a tables with + // columns resource. This means you might get back more permissions than actually pertain to + // the current situation. The table may have separate permissions that also come back. + // + // Thus, for most cases this is just a pass through filter but attempts to clean out + // permissions in the special cases to avoid extra permissions being included. + + if input.Resource.Catalog != nil { + return FilterLakeFormationCatalogPermissions(allPermissions) + } + + if input.Resource.DataLocation != nil { + return FilterLakeFormationDataLocationPermissions(allPermissions) + } + + if input.Resource.Database != nil { + return FilterLakeFormationDatabasePermissions(allPermissions) + } + + if tableType == TableTypeTableWithColumns { + return FilterLakeFormationTableWithColumnsPermissions(input.Resource.Table, columnNames, excludedColumnNames, columnWildcard, allPermissions) + } + + if input.Resource.Table != nil || tableType == TableTypeTable { + return FilterLakeFormationTablePermissions(input.Resource.Table, allPermissions) + } + + return nil +} + +func FilterLakeFormationTablePermissions(table *lakeformation.TableResource, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + // CREATE PERMS (in) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on Table, Name = (Table Name) + // LIST PERMS (out) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, Name = (Table Name) + // LIST PERMS (out) = SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + + // CREATE PERMS (in) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on Table, TableWildcard + // LIST PERMS (out) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, TableWildcard, Name = ALL_TABLES + // LIST PERMS (out) = SELECT on TableWithColumns, Name = ALL_TABLES, ColumnWildcard + + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil { + if aws.StringValue(perm.Resource.TableWithColumns.Name) == aws.StringValue(table.Name) || (table.TableWildcard != nil && aws.StringValue(perm.Resource.TableWithColumns.Name) == TableNameAllTables) { + if len(perm.Permissions) > 0 && aws.StringValue(perm.Permissions[0]) == lakeformation.PermissionSelect { + cleanPermissions = append(cleanPermissions, perm) + continue + } + + if len(perm.PermissionsWithGrantOption) > 0 && aws.StringValue(perm.PermissionsWithGrantOption[0]) == lakeformation.PermissionSelect { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + } + + if perm.Resource.Table != nil && aws.StringValue(perm.Resource.Table.DatabaseName) == aws.StringValue(table.DatabaseName) { + if aws.StringValue(perm.Resource.Table.Name) == aws.StringValue(table.Name) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + + if perm.Resource.Table.TableWildcard != nil && table.TableWildcard != nil { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + continue + } + + return cleanPermissions +} + +func FilterLakeFormationTableWithColumnsPermissions(twc *lakeformation.TableResource, columnNames []*string, excludedColumnNames []*string, columnWildcard bool, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + // CREATE PERMS (in) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + // LIST PERMS (out) = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, Name = (Table Name) + // LIST PERMS (out) = SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnNames != nil { + if StringSlicesEqualIgnoreOrder(perm.Resource.TableWithColumns.ColumnNames, columnNames) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + + if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil && (columnWildcard || len(excludedColumnNames) > 0) { + if perm.Resource.TableWithColumns.ColumnWildcard.ExcludedColumnNames == nil && len(excludedColumnNames) == 0 { + cleanPermissions = append(cleanPermissions, perm) + continue + } + + if len(excludedColumnNames) > 0 && StringSlicesEqualIgnoreOrder(perm.Resource.TableWithColumns.ColumnWildcard.ExcludedColumnNames, excludedColumnNames) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + + if perm.Resource.Table != nil && aws.StringValue(perm.Resource.Table.Name) == aws.StringValue(twc.Name) { + cleanPermissions = append(cleanPermissions, perm) + continue + } + } + + return cleanPermissions +} + +func FilterLakeFormationCatalogPermissions(allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if perm.Resource.Catalog != nil { + cleanPermissions = append(cleanPermissions, perm) + } + } + + return cleanPermissions +} + +func FilterLakeFormationDataLocationPermissions(allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if perm.Resource.DataLocation != nil { + cleanPermissions = append(cleanPermissions, perm) + } + } + + return cleanPermissions +} + +func FilterLakeFormationDatabasePermissions(allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { + var cleanPermissions []*lakeformation.PrincipalResourcePermissions + + for _, perm := range allPermissions { + if perm.Resource.Database != nil { + cleanPermissions = append(cleanPermissions, perm) + } + } + + return cleanPermissions +} + +func StringSlicesEqualIgnoreOrder(s1, s2 []*string) bool { + if len(s1) != len(s2) { + return false + } + + v1 := aws.StringValueSlice(s1) + v2 := aws.StringValueSlice(s2) + + sort.Strings(v1) + sort.Strings(v2) + + return reflect.DeepEqual(v1, v2) +} diff --git a/aws/internal/service/lakeformation/filter_test.go b/aws/internal/service/lakeformation/filter_test.go new file mode 100644 index 00000000000..d44b4cf3877 --- /dev/null +++ b/aws/internal/service/lakeformation/filter_test.go @@ -0,0 +1,546 @@ +package lakeformation + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lakeformation" +) + +func TestFilterPermissions(t *testing.T) { + // primitives to make test cases easier + accountID := "481516234248" + dbName := "Hiliji" + altDBName := "Hiuhbum" + tableName := "Ladocmoc" + + principal := &lakeformation.DataLakePrincipal{ + DataLakePrincipalIdentifier: aws.String(fmt.Sprintf("arn:aws-us-gov:iam::%s:role/Zepotiz-Bulgaria", accountID)), + } + + testCases := []struct { + Name string + Input *lakeformation.ListPermissionsInput + TableType string + ColumnNames []*string + ExcludedColumnNames []*string + ColumnWildcard bool + All []*lakeformation.PrincipalResourcePermissions + ExpectedClean []*lakeformation.PrincipalResourcePermissions + }{ + { + Name: "empty", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{}, + }, + All: nil, + ExpectedClean: nil, + }, + { + Name: "emptyWithInput", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: nil, + ExpectedClean: nil, + }, + { + Name: "wrongTableResource", // this may not actually be possible but we account for it + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(altDBName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: nil, + }, + { + Name: "tableResource", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + { + Name: "tableResourceSelectPerm", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + }, + }, + { + Name: "tableResourceSelectPermGrant", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionSelect}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionAlter}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{lakeformation.PermissionSelect}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + }, + }, + }, + }, + }, + { + Name: "twcBasic", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + TableType: TableTypeTableWithColumns, + ColumnNames: aws.StringSlice([]string{"value"}), + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"value"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"fred"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"value"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + { + Name: "twcWildcard", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + TableType: TableTypeTableWithColumns, + ColumnWildcard: true, + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"fred"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{}, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + { + Name: "twcWildcardExcluded", + Input: &lakeformation.ListPermissionsInput{ + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + TableType: TableTypeTableWithColumns, + ColumnWildcard: true, + ExcludedColumnNames: aws.StringSlice([]string{"value"}), + All: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{ + ExcludedColumnNames: aws.StringSlice([]string{"value"}), + }, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnNames: aws.StringSlice([]string{"fred"}), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + ExpectedClean: []*lakeformation.PrincipalResourcePermissions{ + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionAlter, lakeformation.PermissionDelete}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + Table: &lakeformation.TableResource{ + CatalogId: aws.String(accountID), + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + { + Permissions: aws.StringSlice([]string{lakeformation.PermissionSelect}), + PermissionsWithGrantOption: aws.StringSlice([]string{}), + Principal: principal, + Resource: &lakeformation.Resource{ + TableWithColumns: &lakeformation.TableWithColumnsResource{ + CatalogId: aws.String(accountID), + ColumnWildcard: &lakeformation.ColumnWildcard{ + ExcludedColumnNames: aws.StringSlice([]string{"value"}), + }, + DatabaseName: aws.String(dbName), + Name: aws.String(tableName), + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + got := FilterPermissions(testCase.Input, testCase.TableType, testCase.ColumnNames, testCase.ExcludedColumnNames, testCase.ColumnWildcard, testCase.All) + + if !reflect.DeepEqual(testCase.ExpectedClean, got) { + t.Errorf("got %v, expected %v, input %v", got, testCase.ExpectedClean, testCase.Input) + } + }) + } +} diff --git a/aws/internal/service/lakeformation/waiter/status.go b/aws/internal/service/lakeformation/waiter/status.go new file mode 100644 index 00000000000..fca281e9216 --- /dev/null +++ b/aws/internal/service/lakeformation/waiter/status.go @@ -0,0 +1,48 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/service/lakeformation" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + tflakeformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation" +) + +func PermissionsStatus(conn *lakeformation.LakeFormation, input *lakeformation.ListPermissionsInput, tableType string, columnNames []*string, excludedColumnNames []*string, columnWildcard bool) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + var permissions []*lakeformation.PrincipalResourcePermissions + + err := conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { + for _, permission := range resp.PrincipalResourcePermissions { + if permission == nil { + continue + } + + permissions = append(permissions, permission) + } + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, lakeformation.ErrCodeEntityNotFoundException) { + return nil, StatusNotFound, err + } + + if tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "Invalid principal") { + return nil, StatusIAMDelay, nil + } + + if err != nil { + return nil, StatusFailed, fmt.Errorf("error listing permissions: %w", err) + } + + // clean permissions = filter out permissions that do not pertain to this specific resource + cleanPermissions := tflakeformation.FilterPermissions(input, tableType, columnNames, excludedColumnNames, columnWildcard, permissions) + + if len(cleanPermissions) == 0 { + return nil, StatusNotFound, nil + } + + return permissions, StatusAvailable, nil + } +} diff --git a/aws/internal/service/lakeformation/waiter/waiter.go b/aws/internal/service/lakeformation/waiter/waiter.go new file mode 100644 index 00000000000..80e66a21159 --- /dev/null +++ b/aws/internal/service/lakeformation/waiter/waiter.go @@ -0,0 +1,35 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/lakeformation" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + PermissionsReadyTimeout = 1 * time.Minute + PermissionsDeleteRetryTimeout = 3 * time.Minute + + StatusAvailable = "AVAILABLE" + StatusNotFound = "NOT FOUND" + StatusFailed = "FAILED" + StatusIAMDelay = "IAM DELAY" +) + +func PermissionsReady(conn *lakeformation.LakeFormation, input *lakeformation.ListPermissionsInput, tableType string, columnNames []*string, excludedColumnNames []*string, columnWildcard bool) ([]*lakeformation.PrincipalResourcePermissions, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{StatusNotFound, StatusIAMDelay}, + Target: []string{StatusAvailable}, + Refresh: PermissionsStatus(conn, input, tableType, columnNames, excludedColumnNames, columnWildcard), + Timeout: PermissionsReadyTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.([]*lakeformation.PrincipalResourcePermissions); ok { + return output, err + } + + return nil, err +} diff --git a/aws/resource_aws_lakeformation_permissions.go b/aws/resource_aws_lakeformation_permissions.go index 23286da77b8..93dc4690158 100644 --- a/aws/resource_aws_lakeformation_permissions.go +++ b/aws/resource_aws_lakeformation_permissions.go @@ -4,7 +4,6 @@ import ( "fmt" "log" "reflect" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lakeformation" @@ -14,6 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode" iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + tflakeformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation/waiter" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) @@ -21,7 +22,6 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { return &schema.Resource{ Create: resourceAwsLakeFormationPermissionsCreate, Read: resourceAwsLakeFormationPermissionsRead, - Update: resourceAwsLakeFormationPermissionsCreate, Delete: resourceAwsLakeFormationPermissionsDelete, Schema: map[string]*schema.Schema{ @@ -33,8 +33,9 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "catalog_resource": { Type: schema.TypeBool, - Optional: true, Default: false, + ForceNew: true, + Optional: true, ExactlyOneOf: []string{ "catalog_resource", "data_location", @@ -45,9 +46,10 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "data_location": { Type: schema.TypeList, - Optional: true, Computed: true, + ForceNew: true, MaxItems: 1, + Optional: true, ExactlyOneOf: []string{ "catalog_resource", "data_location", @@ -59,13 +61,15 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { Schema: map[string]*schema.Schema{ "arn": { Type: schema.TypeString, + ForceNew: true, Required: true, ValidateFunc: validateArn, }, "catalog_id": { Type: schema.TypeString, - Optional: true, Computed: true, + ForceNew: true, + Optional: true, ValidateFunc: validateAwsAccountId, }, }, @@ -73,9 +77,10 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "database": { Type: schema.TypeList, - Optional: true, Computed: true, + ForceNew: true, MaxItems: 1, + Optional: true, ExactlyOneOf: []string{ "catalog_resource", "data_location", @@ -87,12 +92,14 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { Schema: map[string]*schema.Schema{ "catalog_id": { Type: schema.TypeString, - Optional: true, Computed: true, + ForceNew: true, + Optional: true, ValidateFunc: validateAwsAccountId, }, "name": { Type: schema.TypeString, + ForceNew: true, Required: true, }, }, @@ -100,9 +107,9 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "permissions": { Type: schema.TypeList, - Required: true, ForceNew: true, MinItems: 1, + Required: true, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringInSlice(lakeformation.Permission_Values(), false), @@ -110,9 +117,9 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "permissions_with_grant_option": { Type: schema.TypeList, - Optional: true, - ForceNew: true, Computed: true, + ForceNew: true, + Optional: true, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringInSlice(lakeformation.Permission_Values(), false), @@ -120,15 +127,16 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "principal": { Type: schema.TypeString, - Required: true, ForceNew: true, + Required: true, ValidateFunc: validatePrincipal, }, "table": { Type: schema.TypeList, - Optional: true, Computed: true, + ForceNew: true, MaxItems: 1, + Optional: true, ExactlyOneOf: []string{ "catalog_resource", "data_location", @@ -140,18 +148,21 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { Schema: map[string]*schema.Schema{ "catalog_id": { Type: schema.TypeString, - Optional: true, Computed: true, + ForceNew: true, + Optional: true, ValidateFunc: validateAwsAccountId, }, "database_name": { Type: schema.TypeString, + ForceNew: true, Required: true, }, "name": { Type: schema.TypeString, - Optional: true, Computed: true, + ForceNew: true, + Optional: true, AtLeastOneOf: []string{ "table.0.name", "table.0.wildcard", @@ -159,8 +170,9 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "wildcard": { Type: schema.TypeBool, - Optional: true, Default: false, + ForceNew: true, + Optional: true, AtLeastOneOf: []string{ "table.0.name", "table.0.wildcard", @@ -171,9 +183,10 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "table_with_columns": { Type: schema.TypeList, - Optional: true, Computed: true, + ForceNew: true, MaxItems: 1, + Optional: true, ExactlyOneOf: []string{ "catalog_resource", "data_location", @@ -185,13 +198,16 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { Schema: map[string]*schema.Schema{ "catalog_id": { Type: schema.TypeString, - Optional: true, Computed: true, + ForceNew: true, + Optional: true, ValidateFunc: validateAwsAccountId, }, "column_names": { - Type: schema.TypeList, + Type: schema.TypeSet, + ForceNew: true, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.NoZeroValues, @@ -203,11 +219,14 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "database_name": { Type: schema.TypeString, + ForceNew: true, Required: true, }, "excluded_column_names": { - Type: schema.TypeList, + Type: schema.TypeSet, + ForceNew: true, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.NoZeroValues, @@ -215,12 +234,14 @@ func resourceAwsLakeFormationPermissions() *schema.Resource { }, "name": { Type: schema.TypeString, + ForceNew: true, Required: true, }, "wildcard": { Type: schema.TypeBool, - Optional: true, Default: false, + ForceNew: true, + Optional: true, AtLeastOneOf: []string{ "table_with_columns.0.column_names", "table_with_columns.0.wildcard", @@ -356,68 +377,59 @@ func resourceAwsLakeFormationPermissionsRead(d *schema.ResourceData, meta interf if v, ok := d.GetOk("table"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { input.Resource.Table = expandLakeFormationTableResource(v.([]interface{})[0].(map[string]interface{})) - tableType = TableTypeTable + tableType = tflakeformation.TableTypeTable } if v, ok := d.GetOk("table_with_columns"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { // can't ListPermissions for TableWithColumns, so use Table instead input.Resource.Table = expandLakeFormationTableWithColumnsResourceAsTable(v.([]interface{})[0].(map[string]interface{})) - tableType = TableTypeTableWithColumns + tableType = tflakeformation.TableTypeTableWithColumns } - log.Printf("[DEBUG] Reading Lake Formation permissions: %v", input) - var allPermissions []*lakeformation.PrincipalResourcePermissions + columnNames := make([]*string, 0) + excludedColumnNames := make([]*string, 0) + columnWildcard := false - err := resource.Retry(iamwaiter.PropagationTimeout, func() *resource.RetryError { - err := conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { - for _, permission := range resp.PrincipalResourcePermissions { - if permission == nil { - continue - } + if tableType == tflakeformation.TableTypeTableWithColumns { + if v, ok := d.GetOk("table_with_columns.0.wildcard"); ok { + columnWildcard = v.(bool) + } - allPermissions = append(allPermissions, permission) + if v, ok := d.GetOk("table_with_columns.0.column_names"); ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + columnNames = expandStringSet(v) } - return !lastPage - }) + } - if err != nil { - if tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "Invalid principal") { - return resource.RetryableError(err) + if v, ok := d.GetOk("table_with_columns.0.excluded_column_names"); ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + excludedColumnNames = expandStringSet(v) } - return resource.NonRetryableError(fmt.Errorf("error reading Lake Formation Permissions: %w", err)) } - return nil - }) + } - if tfresource.TimedOut(err) { - err = conn.ListPermissionsPages(input, func(resp *lakeformation.ListPermissionsOutput, lastPage bool) bool { - for _, permission := range resp.PrincipalResourcePermissions { - if permission == nil { - continue - } + log.Printf("[DEBUG] Reading Lake Formation permissions: %v", input) - allPermissions = append(allPermissions, permission) - } - return !lastPage - }) - } + allPermissions, err := waiter.PermissionsReady(conn, input, tableType, columnNames, excludedColumnNames, columnWildcard) - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, lakeformation.ErrCodeEntityNotFoundException) { - log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } + if !d.IsNewResource() { + if tfawserr.ErrCodeEquals(err, lakeformation.ErrCodeEntityNotFoundException) { + log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } - if !d.IsNewResource() && tfawserr.ErrMessageContains(err, "AccessDeniedException", "Resource does not exist") { - log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state: %s", d.Id(), err) - d.SetId("") - return nil - } + if tfawserr.ErrMessageContains(err, "AccessDeniedException", "Resource does not exist") { + log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state: %s", d.Id(), err) + d.SetId("") + return nil + } - if !d.IsNewResource() && len(allPermissions) == 0 { - log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state (0 permissions)", d.Id()) - d.SetId("") - return nil + if len(allPermissions) == 0 { + log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state (0 permissions)", d.Id()) + d.SetId("") + return nil + } } if err != nil { @@ -425,42 +437,15 @@ func resourceAwsLakeFormationPermissionsRead(d *schema.ResourceData, meta interf } // clean permissions = filter out permissions that do not pertain to this specific resource - - var cleanPermissions []*lakeformation.PrincipalResourcePermissions - - if input.Resource.Catalog != nil { - cleanPermissions = filterLakeFormationCatalogPermissions(allPermissions) - } - - if input.Resource.DataLocation != nil { - cleanPermissions = filterLakeFormationDataLocationPermissions(allPermissions) - } - - if input.Resource.Database != nil { - cleanPermissions = filterLakeFormationDatabasePermissions(allPermissions) - } - - if tableType == TableTypeTable { - cleanPermissions = filterLakeFormationTablePermissions( - aws.StringValue(input.Resource.Table.Name), - input.Resource.Table.TableWildcard != nil, - allPermissions, - ) - } - - if tableType == TableTypeTableWithColumns { - cleanPermissions = filterLakeFormationTableWithColumnsPermissions( - d.Get("table_with_columns.0.database_name").(string), - d.Get("table_with_columns.0.wildcard").(bool), - expandStringList(d.Get("table_with_columns.0.column_names").([]interface{})), - expandStringList(d.Get("table_with_columns.0.excluded_column_names").([]interface{})), - allPermissions, - ) - } + cleanPermissions := tflakeformation.FilterPermissions(input, tableType, columnNames, excludedColumnNames, columnWildcard, allPermissions) if len(cleanPermissions) == 0 { - log.Printf("[WARN] Resource Lake Formation permissions (%s) not found, removing from state", d.Id()) - d.SetId("") + log.Printf("[WARN] No Lake Formation permissions (%s) found", d.Id()) + d.Set("catalog_resource", false) + d.Set("data_location", nil) + d.Set("database", nil) + d.Set("table_with_columns", nil) + d.Set("table", nil) return nil } @@ -474,6 +459,8 @@ func resourceAwsLakeFormationPermissionsRead(d *schema.ResourceData, meta interf if cleanPermissions[0].Resource.Catalog != nil { d.Set("catalog_resource", true) + } else { + d.Set("catalog_resource", false) } if cleanPermissions[0].Resource.DataLocation != nil { @@ -537,7 +524,8 @@ func resourceAwsLakeFormationPermissionsDelete(d *schema.ResourceData, meta inte conn := meta.(*AWSClient).lakeformationconn input := &lakeformation.RevokePermissionsInput{ - Permissions: expandStringList(d.Get("permissions").([]interface{})), + Permissions: expandStringList(d.Get("permissions").([]interface{})), + PermissionsWithGrantOption: expandStringList(d.Get("permissions_with_grant_option").([]interface{})), Principal: &lakeformation.DataLakePrincipal{ DataLakePrincipalIdentifier: aws.String(d.Get("principal").(string)), }, @@ -548,10 +536,6 @@ func resourceAwsLakeFormationPermissionsDelete(d *schema.ResourceData, meta inte input.CatalogId = aws.String(v.(string)) } - if v, ok := d.GetOk("permissions_with_grant_option"); ok { - input.PermissionsWithGrantOption = expandStringList(v.([]interface{})) - } - if _, ok := d.GetOk("catalog_resource"); ok { input.Resource.Catalog = expandLakeFormationCatalogResource() } @@ -578,7 +562,7 @@ func resourceAwsLakeFormationPermissionsDelete(d *schema.ResourceData, meta inte return nil } - err := resource.Retry(2*time.Minute, func() *resource.RetryError { + err := resource.Retry(waiter.PermissionsDeleteRetryTimeout, func() *resource.RetryError { var err error _, err = conn.RevokePermissions(input) if err != nil { @@ -601,127 +585,42 @@ func resourceAwsLakeFormationPermissionsDelete(d *schema.ResourceData, meta inte _, err = conn.RevokePermissions(input) } - if err != nil { - return fmt.Errorf("unable to revoke LakeFormation Permissions (input: %v): %w", input, err) + if tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "No permissions revoked. Grantee has no") { + return nil } - return nil -} - -const ( - TableNameAllTables = "ALL_TABLES" - TableTypeTable = "Table" - TableTypeTableWithColumns = "TableWithColumns" -) - -func filterLakeFormationTablePermissions(tableName string, tableWildcard bool, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { - // CREATE PERMS = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on Table, Name = (Table Name) - // LIST PERMS = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, Name = (Table Name) - // LIST PERMS = SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard - - // CREATE PERMS = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on Table, TableWildcard - // LIST PERMS = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, TableWildcard, Name = ALL_TABLES - // LIST PERMS = SELECT on TableWithColumns, Name = ALL_TABLES, ColumnWildcard - - var cleanPermissions []*lakeformation.PrincipalResourcePermissions - - for _, perm := range allPermissions { - if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil { - if aws.StringValue(perm.Resource.TableWithColumns.Name) == tableName || (tableWildcard && aws.StringValue(perm.Resource.TableWithColumns.Name) == TableNameAllTables) { - if len(perm.Permissions) > 0 && aws.StringValue(perm.Permissions[0]) == lakeformation.PermissionSelect { - cleanPermissions = append(cleanPermissions, perm) - continue - } - - if len(perm.PermissionsWithGrantOption) > 0 && aws.StringValue(perm.PermissionsWithGrantOption[0]) == lakeformation.PermissionSelect { - cleanPermissions = append(cleanPermissions, perm) - continue - } - } - } - - if perm.Resource.Table != nil { - if aws.StringValue(perm.Resource.Table.Name) == tableName { - cleanPermissions = append(cleanPermissions, perm) - continue - } - - if perm.Resource.Table.TableWildcard != nil && tableWildcard { - cleanPermissions = append(cleanPermissions, perm) - continue - } - } - continue + if err != nil { + return fmt.Errorf("unable to revoke LakeFormation Permissions (input: %v): %w", input, err) } - return cleanPermissions -} - -func filterLakeFormationTableWithColumnsPermissions(tableName string, columnWildcard bool, columnNames []*string, excludedColumnNames []*string, allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { - // CREATE PERMS = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT, SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard - // LIST PERMS = ALL, ALTER, DELETE, DESCRIBE, DROP, INSERT on Table, Name = (Table Name) - // LIST PERMS = SELECT on TableWithColumns, Name = (Table Name), ColumnWildcard + // Attempted to add a waiter here to wait for delete to complete. However, ListPermissions() returns + // permissions, at least for catalog/CREATE_DATABASE permission, even if they do not exist. That makes + // knowing when the delete is complete impossible. Instead, we'll retry until we get the right error. - var cleanPermissions []*lakeformation.PrincipalResourcePermissions + // Knowing when the delete is complete is complicated: + // You can't just wait until permissions = 0 because there could be many other unrelated permissions + // on the resource and filtering is non-trivial for table with columns. - for _, perm := range allPermissions { - if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnNames != nil { - if StringSlicesEqualIgnoreOrder(perm.Resource.TableWithColumns.ColumnNames, columnNames) { - cleanPermissions = append(cleanPermissions, perm) - continue - } - } - - if perm.Resource.TableWithColumns != nil && perm.Resource.TableWithColumns.ColumnWildcard != nil && (columnWildcard || len(excludedColumnNames) > 0) { - if (perm.Resource.TableWithColumns.ColumnWildcard.ExcludedColumnNames == nil && len(excludedColumnNames) == 0) || StringSlicesEqualIgnoreOrder(perm.Resource.TableWithColumns.ColumnWildcard.ExcludedColumnNames, excludedColumnNames) { - cleanPermissions = append(cleanPermissions, perm) - continue - } - } - - if perm.Resource.Table != nil && aws.StringValue(perm.Resource.Table.Name) == tableName { - cleanPermissions = append(cleanPermissions, perm) - continue - } - } - - return cleanPermissions -} - -func filterLakeFormationCatalogPermissions(allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { - var cleanPermissions []*lakeformation.PrincipalResourcePermissions + err = resource.Retry(waiter.PermissionsDeleteRetryTimeout, func() *resource.RetryError { + var err error + _, err = conn.RevokePermissions(input) - for _, perm := range allPermissions { - if perm.Resource.Catalog != nil { - cleanPermissions = append(cleanPermissions, perm) + if !tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "No permissions revoked. Grantee has no") { + return resource.RetryableError(err) } - } - return cleanPermissions -} - -func filterLakeFormationDataLocationPermissions(allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { - var cleanPermissions []*lakeformation.PrincipalResourcePermissions + return nil + }) - for _, perm := range allPermissions { - if perm.Resource.DataLocation != nil { - cleanPermissions = append(cleanPermissions, perm) - } + if tfresource.TimedOut(err) { + _, err = conn.RevokePermissions(input) } - return cleanPermissions -} - -func filterLakeFormationDatabasePermissions(allPermissions []*lakeformation.PrincipalResourcePermissions) []*lakeformation.PrincipalResourcePermissions { - var cleanPermissions []*lakeformation.PrincipalResourcePermissions - - for _, perm := range allPermissions { - if perm.Resource.Database != nil { - cleanPermissions = append(cleanPermissions, perm) - } + if err != nil && !tfawserr.ErrMessageContains(err, lakeformation.ErrCodeInvalidInputException, "No permissions revoked. Grantee has no") { + return fmt.Errorf("unable to revoke LakeFormation Permissions (input: %v): %w", input, err) } - return cleanPermissions + return nil } func expandLakeFormationCatalogResource() *lakeformation.CatalogResource { @@ -864,7 +763,7 @@ func flattenLakeFormationTableResource(apiObject *lakeformation.TableResource) m } if v := apiObject.Name; v != nil { - if aws.StringValue(v) != TableNameAllTables || apiObject.TableWildcard == nil { + if aws.StringValue(v) != tflakeformation.TableNameAllTables || apiObject.TableWildcard == nil { tfMap["name"] = aws.StringValue(v) } } @@ -887,17 +786,21 @@ func expandLakeFormationTableWithColumnsResource(tfMap map[string]interface{}) * apiObject.CatalogId = aws.String(v) } - if v, ok := tfMap["column_names"]; ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - apiObject.ColumnNames = expandStringList(v.([]interface{})) + if v, ok := tfMap["column_names"]; ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + apiObject.ColumnNames = expandStringSet(v) + } } if v, ok := tfMap["database_name"].(string); ok && v != "" { apiObject.DatabaseName = aws.String(v) } - if v, ok := tfMap["excluded_column_names"]; ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - apiObject.ColumnWildcard = &lakeformation.ColumnWildcard{ - ExcludedColumnNames: expandStringList(v.([]interface{})), + if v, ok := tfMap["excluded_column_names"]; ok { + if v, ok := v.(*schema.Set); ok && v.Len() > 0 { + apiObject.ColumnWildcard = &lakeformation.ColumnWildcard{ + ExcludedColumnNames: expandStringSet(v), + } } } @@ -923,7 +826,7 @@ func flattenLakeFormationTableWithColumnsResource(apiObject *lakeformation.Table tfMap["catalog_id"] = aws.StringValue(v) } - tfMap["column_names"] = flattenStringList(apiObject.ColumnNames) + tfMap["column_names"] = flattenStringSet(apiObject.ColumnNames) if v := apiObject.DatabaseName; v != nil { tfMap["database_name"] = aws.StringValue(v) @@ -931,7 +834,7 @@ func flattenLakeFormationTableWithColumnsResource(apiObject *lakeformation.Table if v := apiObject.ColumnWildcard; v != nil { tfMap["wildcard"] = true - tfMap["excluded_column_names"] = flattenStringList(v.ExcludedColumnNames) + tfMap["excluded_column_names"] = flattenStringSet(v.ExcludedColumnNames) } if v := apiObject.Name; v != nil { diff --git a/aws/resource_aws_lakeformation_permissions_test.go b/aws/resource_aws_lakeformation_permissions_test.go index 69857161931..bfd87e3fd9f 100644 --- a/aws/resource_aws_lakeformation_permissions_test.go +++ b/aws/resource_aws_lakeformation_permissions_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" iamwaiter "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter" + tflakeformation "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/lakeformation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) @@ -41,6 +42,28 @@ func testAccAWSLakeFormationPermissions_basic(t *testing.T) { }) } +func testAccAWSLakeFormationPermissions_disappears(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_lakeformation_permissions.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName, "\"event\", \"timestamp\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLakeFormationPermissionsExists(resourceName), + testAccCheckResourceDisappears(testAccProvider, resourceAwsLakeFormationPermissions(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccAWSLakeFormationPermissions_dataLocation(t *testing.T) { rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_lakeformation_permissions.test" @@ -169,7 +192,22 @@ func testAccAWSLakeFormationPermissions_tableWithColumns(t *testing.T) { CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName), + Config: testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName, "\"event\", \"timestamp\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLakeFormationPermissionsExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "principal", roleName, "arn"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "table_with_columns.0.database_name", tableName, "database_name"), + resource.TestCheckResourceAttrPair(resourceName, "table_with_columns.0.name", tableName, "name"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.#", "2"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.0", "event"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.1", "timestamp"), + resource.TestCheckResourceAttr(resourceName, "permissions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "permissions.0", lakeformation.PermissionSelect), + ), + }, + { + Config: testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName, "\"timestamp\", \"event\""), Check: resource.ComposeTestCheckFunc( testAccCheckAWSLakeFormationPermissionsExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "principal", roleName, "arn"), @@ -183,6 +221,36 @@ func testAccAWSLakeFormationPermissions_tableWithColumns(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "permissions.0", lakeformation.PermissionSelect), ), }, + { + Config: testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName, "\"timestamp\", \"event\", \"transactionamount\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLakeFormationPermissionsExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "principal", roleName, "arn"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "table_with_columns.0.database_name", tableName, "database_name"), + resource.TestCheckResourceAttrPair(resourceName, "table_with_columns.0.name", tableName, "name"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.#", "3"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.0", "event"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.1", "timestamp"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.2", "transactionamount"), + resource.TestCheckResourceAttr(resourceName, "permissions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "permissions.0", lakeformation.PermissionSelect), + ), + }, + { + Config: testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName, "\"event\""), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLakeFormationPermissionsExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "principal", roleName, "arn"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "table_with_columns.0.database_name", tableName, "database_name"), + resource.TestCheckResourceAttrPair(resourceName, "table_with_columns.0.name", tableName, "name"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "table_with_columns.0.column_names.0", "event"), + resource.TestCheckResourceAttr(resourceName, "permissions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "permissions.0", lakeformation.PermissionSelect), + ), + }, }, }) } @@ -316,6 +384,30 @@ func testAccAWSLakeFormationPermissions_columnWildcardPermissions(t *testing.T) }) } +func testAccAWSLakeFormationPermissions_columnWildcardExcludedColumnsPermissions(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_lakeformation_permissions.test" + roleName := "aws_iam_role.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPartitionHasServicePreCheck(lakeformation.EndpointsID, t) }, + ErrorCheck: testAccErrorCheck(t, lakeformation.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLakeFormationPermissionsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLakeFormationPermissionsConfig_columnWildcardExcludedColumnsPermissions(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLakeFormationPermissionsExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "principal", roleName, "arn"), + resource.TestCheckResourceAttr(resourceName, "permissions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "permissions_with_grant_option.#", "0"), + ), + }, + }, + }) +} + func testAccCheckAWSLakeFormationPermissionsDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).lakeformationconn @@ -410,7 +502,7 @@ func permissionCountForLakeFormationResource(conn *lakeformation.LakeFormation, tableType := "" if v, ok := rs.Primary.Attributes["table.#"]; ok && v != "" && v != "0" { - tableType = TableTypeTable + tableType = tflakeformation.TableTypeTable tfMap := map[string]interface{}{} @@ -422,7 +514,7 @@ func permissionCountForLakeFormationResource(conn *lakeformation.LakeFormation, tfMap["database_name"] = v } - if v := rs.Primary.Attributes["table.0.name"]; v != "" && v != TableNameAllTables { + if v := rs.Primary.Attributes["table.0.name"]; v != "" && v != tflakeformation.TableNameAllTables { tfMap["name"] = v } @@ -434,7 +526,7 @@ func permissionCountForLakeFormationResource(conn *lakeformation.LakeFormation, } if v, ok := rs.Primary.Attributes["table_with_columns.#"]; ok && v != "" && v != "0" { - tableType = TableTypeTableWithColumns + tableType = tflakeformation.TableTypeTableWithColumns tfMap := map[string]interface{}{} @@ -503,53 +595,46 @@ func permissionCountForLakeFormationResource(conn *lakeformation.LakeFormation, return 0, fmt.Errorf("error listing Lake Formation permissions after retry: %w", err) } - // clean permissions = filter out permissions that do not pertain to this specific resource + columnNames := make([]*string, 0) + excludedColumnNames := make([]*string, 0) + columnWildcard := false - var cleanPermissions []*lakeformation.PrincipalResourcePermissions + if tableType == tflakeformation.TableTypeTableWithColumns { + if v := rs.Primary.Attributes["table_with_columns.0.wildcard"]; v != "" && v == "true" { + columnWildcard = true + } - if input.Resource.Catalog != nil { - cleanPermissions = filterLakeFormationCatalogPermissions(allPermissions) - } + colCount := 0 - if input.Resource.DataLocation != nil { - cleanPermissions = filterLakeFormationDataLocationPermissions(allPermissions) - } + if v := rs.Primary.Attributes["table_with_columns.0.column_names.#"]; v != "" { + colCount, err = strconv.Atoi(rs.Primary.Attributes["table_with_columns.0.column_names.#"]) - if input.Resource.Database != nil { - cleanPermissions = filterLakeFormationDatabasePermissions(allPermissions) - } + if err != nil { + return 0, fmt.Errorf("could not convert string (%s) Atoi for column_names: %w", rs.Primary.Attributes["table_with_columns.0.column_names.#"], err) + } + } - if tableType == TableTypeTable { - cleanPermissions = filterLakeFormationTablePermissions( - aws.StringValue(input.Resource.Table.Name), - input.Resource.Table.TableWildcard != nil, - allPermissions, - ) - } + for i := 0; i < colCount; i++ { + columnNames = append(columnNames, aws.String(rs.Primary.Attributes[fmt.Sprintf("table_with_columns.0.column_names.%d", i)])) + } + + colCount = 0 + + if v := rs.Primary.Attributes["table_with_columns.0.excluded_column_names.#"]; v != "" { + colCount, err = strconv.Atoi(rs.Primary.Attributes["table_with_columns.0.excluded_column_names.#"]) - var columnNames []string - if cols, err := strconv.Atoi(rs.Primary.Attributes["table_with_columns.0.column_names.#"]); err == nil { - for i := 0; i < cols; i++ { - columnNames = append(columnNames, rs.Primary.Attributes[fmt.Sprintf("table_with_columns.0.column_names.%d", i)]) + if err != nil { + return 0, fmt.Errorf("could not convert string (%s) Atoi for excluded_column_names: %w", rs.Primary.Attributes["table_with_columns.0.excluded_column_names.#"], err) + } } - } - var excludedColumnNames []string - if cols, err := strconv.Atoi(rs.Primary.Attributes["table_with_columns.0.excluded_column_names.#"]); err == nil { - for i := 0; i < cols; i++ { - excludedColumnNames = append(excludedColumnNames, rs.Primary.Attributes[fmt.Sprintf("table_with_columns.0.excluded_column_names.%d", i)]) + for i := 0; i < colCount; i++ { + excludedColumnNames = append(excludedColumnNames, aws.String(rs.Primary.Attributes[fmt.Sprintf("table_with_columns.0.excluded_column_names.%d", i)])) } } - if tableType == TableTypeTableWithColumns { - cleanPermissions = filterLakeFormationTableWithColumnsPermissions( - rs.Primary.Attributes["table_with_columns.0.database_name"], - rs.Primary.Attributes["table_with_columns.0.wildcard"] == "true", - aws.StringSlice(columnNames), - aws.StringSlice(excludedColumnNames), - allPermissions, - ) - } + // clean permissions = filter out permissions that do not pertain to this specific resource + cleanPermissions := tflakeformation.FilterPermissions(input, tableType, columnNames, excludedColumnNames, columnWildcard, allPermissions) return len(cleanPermissions), nil } @@ -614,7 +699,15 @@ resource "aws_iam_role" "test" { }, "Effect": "Allow", "Sid": "" - } + }, + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + } ] } EOF @@ -627,7 +720,8 @@ resource "aws_s3_bucket" "test" { } resource "aws_lakeformation_resource" "test" { - arn = aws_s3_bucket.test.arn + arn = aws_s3_bucket.test.arn + role_arn = aws_iam_role.test.arn } data "aws_caller_identity" "current" {} @@ -806,7 +900,7 @@ resource "aws_lakeformation_permissions" "test" { `, rName) } -func testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName string) string { +func testAccAWSLakeFormationPermissionsConfig_tableWithColumns(rName string, columns string) string { return fmt.Sprintf(` data "aws_partition" "current" {} @@ -853,7 +947,7 @@ resource "aws_glue_catalog_table" "test" { } columns { - name = "value" + name = "transactionamount" type = "double" } } @@ -870,13 +964,13 @@ resource "aws_lakeformation_permissions" "test" { table_with_columns { database_name = aws_glue_catalog_table.test.database_name name = aws_glue_catalog_table.test.name - column_names = ["event", "timestamp"] + column_names = [%[2]s] } # for consistency, ensure that admins are setup before testing depends_on = [aws_lakeformation_data_lake_settings.test] } -`, rName) +`, rName, columns) } func testAccAWSLakeFormationPermissionsConfig_implicitTableWithColumnsPermissions(rName string) string { @@ -1248,3 +1342,77 @@ resource "aws_lakeformation_permissions" "test" { } `, rName) } + +func testAccAWSLakeFormationPermissionsConfig_columnWildcardExcludedColumnsPermissions(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/" + + assume_role_policy = <