diff --git a/.changelog/11941.txt b/.changelog/11941.txt new file mode 100644 index 00000000000..b5428f79a4d --- /dev/null +++ b/.changelog/11941.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_lambda_layer_version_permission +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 417c3cba0bb..e6e27bec79e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1301,6 +1301,7 @@ func Provider() *schema.Provider { "aws_lambda_function": lambda.ResourceFunction(), "aws_lambda_function_event_invoke_config": lambda.ResourceFunctionEventInvokeConfig(), "aws_lambda_layer_version": lambda.ResourceLayerVersion(), + "aws_lambda_layer_version_permission": lambda.ResourceLayerVersionPermission(), "aws_lambda_permission": lambda.ResourcePermission(), "aws_lambda_provisioned_concurrency_config": lambda.ResourceProvisionedConcurrencyConfig(), diff --git a/internal/service/lambda/layer_version_permission.go b/internal/service/lambda/layer_version_permission.go new file mode 100644 index 00000000000..5f858a18830 --- /dev/null +++ b/internal/service/lambda/layer_version_permission.go @@ -0,0 +1,234 @@ +package lambda + +import ( + "encoding/json" + "fmt" + "log" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/service/iam" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceLayerVersionPermission() *schema.Resource { + return &schema.Resource{ + Create: resourceLayerVersionPermissionCreate, + Read: resourceLayerVersionPermissionRead, + Delete: resourceLayerVersionPermissionDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "layer_name": { + Type: schema.TypeString, + ValidateFunc: validation.Any( + validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9-_]+$`), ""), + verify.ValidARN, + ), + Required: true, + ForceNew: true, + }, + "version_number": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "statement_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "action": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "principal": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "organization_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "revision_id": { + Type: schema.TypeString, + Computed: true, + }, + "policy": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceLayerVersionPermissionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).LambdaConn + + layerName := d.Get("layer_name").(string) + versionNumber := d.Get("version_number").(int) + + params := &lambda.AddLayerVersionPermissionInput{ + LayerName: aws.String(layerName), + VersionNumber: aws.Int64(int64(versionNumber)), + Action: aws.String(d.Get("action").(string)), + Principal: aws.String(d.Get("principal").(string)), + StatementId: aws.String(d.Get("statement_id").(string)), + } + + if v, ok := d.GetOk("organization_id"); ok { + params.OrganizationId = aws.String(v.(string)) + } + + _, err := conn.AddLayerVersionPermission(params) + if err != nil { + return fmt.Errorf("error adding Lambda Layer Version Permission (layer: %s, version: %d): %w", layerName, versionNumber, err) + } + + d.SetId(fmt.Sprintf("%s,%d", layerName, versionNumber)) + + return resourceLayerVersionPermissionRead(d, meta) +} + +func resourceLayerVersionPermissionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).LambdaConn + + layerName, versionNumber, err := ResourceLayerVersionPermissionParseId(d.Id()) + if err != nil { + return err + } + + input := &lambda.GetLayerVersionPolicyInput{ + LayerName: aws.String(layerName), + VersionNumber: aws.Int64(versionNumber), + } + + layerVersionPolicyOutput, err := conn.GetLayerVersionPolicy(input) + + if tfawserr.ErrCodeEquals(err, lambda.ErrCodeResourceNotFoundException) { + log.Printf("[WARN] Lambda Layer Version Permission (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Lambda Layer Version Permission (%s): %w", d.Id(), err) + } + + policyDoc := &iam.IAMPolicyDoc{} + + if err := json.Unmarshal([]byte(aws.StringValue(layerVersionPolicyOutput.Policy)), policyDoc); err != nil { + return err + } + + d.Set("layer_name", layerName) + d.Set("version_number", versionNumber) + d.Set("policy", layerVersionPolicyOutput.Policy) + d.Set("revision_id", layerVersionPolicyOutput.RevisionId) + + if policyDoc != nil && len(policyDoc.Statements) > 0 { + d.Set("statement_id", policyDoc.Statements[0].Sid) + + if actions := policyDoc.Statements[0].Actions; actions != nil { + var action string + t := reflect.TypeOf(actions) + if t.String() == "[]string" && len(actions.([]string)) > 0 { + action = actions.([]string)[0] + } else if t.String() == "string" { + action = actions.(string) + } + + d.Set("action", action) + } + + if len(policyDoc.Statements[0].Conditions) > 0 && policyDoc.Statements[0].Conditions[0].Values != nil { + var organizationId string + values := policyDoc.Statements[0].Conditions[0].Values + t := reflect.TypeOf(values) + if t.String() == "[]string" && len(values.([]string)) > 0 { + organizationId = values.([]string)[0] + } else if t.String() == "string" { + organizationId = values.(string) + } + + d.Set("organization_id", organizationId) + } + + if len(policyDoc.Statements[0].Principals) > 0 && policyDoc.Statements[0].Principals[0].Identifiers != nil { + var principal string + identifiers := policyDoc.Statements[0].Principals[0].Identifiers + t := reflect.TypeOf(identifiers) + if t.String() == "[]string" && len(identifiers.([]string)) > 0 && identifiers.([]string)[0] == "*" { + principal = "*" + } else if t.String() == "string" { + policyPrincipalArn, err := arn.Parse(identifiers.(string)) + if err != nil { + return fmt.Errorf("error reading Principal ARN from Lambda Layer Version Permission (%s): %w", d.Id(), err) + } + principal = policyPrincipalArn.AccountID + } + + d.Set("principal", principal) + } + } + + return nil +} + +func resourceLayerVersionPermissionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).LambdaConn + + layerName, versionNumber, err := ResourceLayerVersionPermissionParseId(d.Id()) + if err != nil { + return err + } + + input := &lambda.RemoveLayerVersionPermissionInput{ + LayerName: aws.String(layerName), + VersionNumber: aws.Int64(versionNumber), + StatementId: aws.String(d.Get("statement_id").(string)), + } + + _, err = conn.RemoveLayerVersionPermission(input) + + if tfawserr.ErrCodeEquals(err, lambda.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Lambda Layer Version Permission (%s): %w", d.Id(), err) + } + + return nil +} + +func ResourceLayerVersionPermissionParseId(id string) (string, int64, error) { + parts := strings.Split(id, ",") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", 0, fmt.Errorf("unexpected format of ID (%s), expected LAYER_NAME,VERSION_NUMBER or LAYER_ARN,VERSION_NUMBER", id) + } + + layerName := parts[0] + versionNum, err := strconv.ParseInt(parts[1], 10, 64) + + if err != nil { + return "", 0, err + } + + return layerName, versionNum, nil +} diff --git a/internal/service/lambda/layer_version_permission_test.go b/internal/service/lambda/layer_version_permission_test.go new file mode 100644 index 00000000000..8fe43492ccf --- /dev/null +++ b/internal/service/lambda/layer_version_permission_test.go @@ -0,0 +1,288 @@ +package lambda_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tflambda "github.com/hashicorp/terraform-provider-aws/internal/service/lambda" +) + +func TestAccLambdaLayerVersionPermission_basic_byARN(t *testing.T) { + resourceName := "aws_lambda_layer_version_permission.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, lambda.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckLambdaLayerVersionPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testLayerVersionPermission_basic_arn(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaLayerVersionPermissionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "action", "lambda:GetLayerVersion"), + resource.TestCheckResourceAttr(resourceName, "principal", "*"), + resource.TestCheckResourceAttr(resourceName, "statement_id", "xaccount"), + resource.TestCheckResourceAttrPair(resourceName, "layer_name", "aws_lambda_layer_version.test", "layer_arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLambdaLayerVersionPermission_basic_byName(t *testing.T) { + resourceName := "aws_lambda_layer_version_permission.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, lambda.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckLambdaLayerVersionPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testLayerVersionPermission_basic_name(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaLayerVersionPermissionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "action", "lambda:GetLayerVersion"), + resource.TestCheckResourceAttr(resourceName, "principal", "*"), + resource.TestCheckResourceAttr(resourceName, "statement_id", "xaccount"), + resource.TestCheckResourceAttrPair(resourceName, "layer_name", "aws_lambda_layer_version.test", "layer_name"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLambdaLayerVersionPermission_org(t *testing.T) { + resourceName := "aws_lambda_layer_version_permission.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, lambda.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckLambdaLayerVersionPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testLayerVersionPermission_org(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaLayerVersionPermissionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "action", "lambda:GetLayerVersion"), + resource.TestCheckResourceAttr(resourceName, "principal", "*"), + resource.TestCheckResourceAttr(resourceName, "statement_id", "xaccount"), + resource.TestCheckResourceAttr(resourceName, "organization_id", "o-0123456789"), + resource.TestCheckResourceAttrPair(resourceName, "layer_name", "aws_lambda_layer_version.test", "layer_arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLambdaLayerVersionPermission_account(t *testing.T) { + resourceName := "aws_lambda_layer_version_permission.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, lambda.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckLambdaLayerVersionPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testLayerVersionPermission_account(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaLayerVersionPermissionExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "action", "lambda:GetLayerVersion"), + resource.TestCheckResourceAttrPair(resourceName, "principal", "data.aws_caller_identity.current", "account_id"), + resource.TestCheckResourceAttr(resourceName, "statement_id", "xaccount"), + resource.TestCheckResourceAttrPair(resourceName, "layer_name", "aws_lambda_layer_version.test", "layer_arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLambdaLayerVersionPermission_disappears(t *testing.T) { + resourceName := "aws_lambda_layer_version_permission.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, lambda.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckLambdaLayerVersionPermissionDestroy, + Steps: []resource.TestStep{ + { + Config: testLayerVersionPermission_account(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaLayerVersionPermissionExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tflambda.ResourceLayerVersionPermission(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +// Creating Lambda layer and Lambda layer permissions + +func testLayerVersionPermission_basic_arn(layerName string) string { + return fmt.Sprintf(` +resource "aws_lambda_layer_version" "test" { + filename = "test-fixtures/lambdatest.zip" + layer_name = %[1]q +} + +resource "aws_lambda_layer_version_permission" "test" { + layer_name = aws_lambda_layer_version.test.layer_arn + version_number = aws_lambda_layer_version.test.version + action = "lambda:GetLayerVersion" + statement_id = "xaccount" + principal = "*" +} +`, layerName) +} + +func testLayerVersionPermission_basic_name(layerName string) string { + return fmt.Sprintf(` +resource "aws_lambda_layer_version" "test" { + filename = "test-fixtures/lambdatest.zip" + layer_name = %[1]q +} + +resource "aws_lambda_layer_version_permission" "test" { + layer_name = aws_lambda_layer_version.test.layer_name + version_number = aws_lambda_layer_version.test.version + action = "lambda:GetLayerVersion" + statement_id = "xaccount" + principal = "*" +} +`, layerName) +} + +func testLayerVersionPermission_org(layerName string) string { + return fmt.Sprintf(` +resource "aws_lambda_layer_version" "test" { + filename = "test-fixtures/lambdatest.zip" + layer_name = "%s" +} + +resource "aws_lambda_layer_version_permission" "test" { + layer_name = aws_lambda_layer_version.test.layer_arn + version_number = aws_lambda_layer_version.test.version + action = "lambda:GetLayerVersion" + statement_id = "xaccount" + principal = "*" + organization_id = "o-0123456789" +} +`, layerName) +} + +func testLayerVersionPermission_account(layerName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_lambda_layer_version" "test" { + filename = "test-fixtures/lambdatest.zip" + layer_name = "%s" +} + +resource "aws_lambda_layer_version_permission" "test" { + layer_name = aws_lambda_layer_version.test.layer_arn + version_number = aws_lambda_layer_version.test.version + action = "lambda:GetLayerVersion" + statement_id = "xaccount" + principal = data.aws_caller_identity.current.account_id +} +`, layerName) +} + +func testAccCheckAwsLambdaLayerVersionPermissionExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Lambda Layer version policy ID not set") + } + + layerName, versionNumber, err := tflambda.ResourceLayerVersionPermissionParseId(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error parsing lambda layer ID: %w", err) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaConn + + _, err = conn.GetLayerVersionPolicy(&lambda.GetLayerVersionPolicyInput{ + LayerName: aws.String(layerName), + VersionNumber: aws.Int64(versionNumber), + }) + + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckLambdaLayerVersionPermissionDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lambda_layer_version_permission" { + continue + } + + layerName, versionNumber, err := tflambda.ResourceLayerVersionPermissionParseId(rs.Primary.ID) + if err != nil { + return err + } + + _, err = conn.GetLayerVersionPolicy(&lambda.GetLayerVersionPolicyInput{ + LayerName: aws.String(layerName), + VersionNumber: aws.Int64(versionNumber), + }) + + if tfawserr.ErrCodeEquals(err, lambda.ErrCodeResourceNotFoundException) { + continue + } + + if err != nil { + return err + } + } + return nil +} diff --git a/website/docs/r/lambda_layer_version_permission.html.markdown b/website/docs/r/lambda_layer_version_permission.html.markdown new file mode 100644 index 00000000000..94211f1709e --- /dev/null +++ b/website/docs/r/lambda_layer_version_permission.html.markdown @@ -0,0 +1,54 @@ +--- +subcategory: "Lambda" +layout: "aws" +page_title: "AWS: aws_lambda_layer_version_permission" +description: |- + Provides a Lambda Layer Version Permission resource. +--- + +# Resource: aws_lambda_layer_version_permission + +Provides a Lambda Layer Version Permission resource. It allows you to share you own Lambda Layers to another account by account ID, to all accounts in AWS organization or even to all AWS accounts. + +For information about Lambda Layer Permissions and how to use them, see [Using Resource-based Policies for AWS Lambda][1] + +## Example Usage + +```terraform +resource "aws_lambda_layer_version_permission" "lambda_layer_permission" { + layer_name = "arn:aws:lambda:us-west-2:123456654321:layer:test_layer1" + version_number = 1 + principal = "111111111111" + action = "lambda:GetLayerVersion" + statement_id = "dev-account" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `action` - (Required) Action, which will be allowed. `lambda:GetLayerVersion` value is suggested by AWS documantation. +* `layer_name` (Required) The name or ARN of the Lambda Layer, which you want to grant access to. +* `organization_id` - (Optional) An identifier of AWS Organization, which should be able to use your Lambda Layer. `principal` should be equal to `*` if `organization_id` provided. +* `principal` - (Required) AWS account ID which should be able to use your Lambda Layer. `*` can be used here, if you want to share your Lambda Layer widely. +* `statement_id` - (Required) The name of Lambda Layer Permission, for example `dev-account` - human readable note about what is this permission for. +* `version_number` (Required) Version of Lambda Layer, which you want to grant access to. Note: permissions only apply to a single version of a layer. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The `layer_name` and `version_number`, separated by a comma (`,`). +* `revision_id` - A unique identifier for the current revision of the policy. +* `policy` - Full Lambda Layer Permission policy. + +## Import + +Lambda Layer Permissions can be imported using `layer_name` and `version_number`, separated by a comma (`,`). + +```sh +$ terraform import aws_lambda_layer_version_permission.example arn:aws:lambda:us-west-2:123456654321:layer:test_layer1,1 +``` + +[1]: https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-xaccountlayer