diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 3b7bb79f6..b26cbc503 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -174,6 +174,7 @@ func Provider() terraform.ResourceProvider { "aws_lambda_function": resourceAwsLambdaFunction(), "aws_lambda_event_source_mapping": resourceAwsLambdaEventSourceMapping(), "aws_lambda_alias": resourceAwsLambdaAlias(), + "aws_lambda_permission": resourceAwsLambdaPermission(), "aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), diff --git a/builtin/providers/aws/resource_aws_lambda_permission.go b/builtin/providers/aws/resource_aws_lambda_permission.go new file mode 100644 index 000000000..6019d79de --- /dev/null +++ b/builtin/providers/aws/resource_aws_lambda_permission.go @@ -0,0 +1,251 @@ +package aws + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +var LambdaFunctionRegexp = `^(arn:aws:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$` + +func resourceAwsLambdaPermission() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsLambdaPermissionCreate, + Read: resourceAwsLambdaPermissionRead, + Delete: resourceAwsLambdaPermissionDelete, + + Schema: map[string]*schema.Schema{ + "action": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateLambdaPermissionAction, + }, + "function_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateLambdaFunctionName, + }, + "principal": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "qualifier": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateLambdaQualifier, + }, + "source_account": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateAwsAccountId, + }, + "source_arn": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + "statement_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validatePolicyStatementId, + }, + }, + } +} + +func resourceAwsLambdaPermissionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lambdaconn + + input := lambda.AddPermissionInput{ + Action: aws.String(d.Get("action").(string)), + FunctionName: aws.String(d.Get("function_name").(string)), + Principal: aws.String(d.Get("principal").(string)), + StatementId: aws.String(d.Get("statement_id").(string)), + } + + if v, ok := d.GetOk("qualifier"); ok { + input.Qualifier = aws.String(v.(string)) + } + if v, ok := d.GetOk("source_account"); ok { + input.SourceAccount = aws.String(v.(string)) + } + if v, ok := d.GetOk("source_arn"); ok { + input.SourceArn = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Adding new Lambda permission: %s", input) + var out *lambda.AddPermissionOutput + err := resource.Retry(1*time.Minute, func() error { + var err error + out, err = conn.AddPermission(&input) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok { + // IAM is eventually consistent :/ + if awsErr.Code() == "ResourceConflictException" { + return fmt.Errorf("[WARN] Error creating ELB Listener with SSL Cert, retrying: %s", err) + } + } + return resource.RetryError{Err: err} + } + return nil + }) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Created new Lambda permission: %s", *out.Statement) + + d.SetId(d.Get("statement_id").(string)) + + return resourceAwsLambdaPermissionRead(d, meta) +} + +func resourceAwsLambdaPermissionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lambdaconn + + input := lambda.GetPolicyInput{ + FunctionName: aws.String(d.Get("function_name").(string)), + } + if v, ok := d.GetOk("qualifier"); ok { + input.Qualifier = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Looking for Lambda permission: %s", input) + out, err := conn.GetPolicy(&input) + if err != nil { + return fmt.Errorf("Error reading Lambda policy: %s", err) + } + + d.Set("full_policy", *out.Policy) + policyInBytes := []byte(*out.Policy) + policy := LambdaPolicy{} + err = json.Unmarshal(policyInBytes, &policy) + if err != nil { + return fmt.Errorf("Error unmarshalling Lambda policy: %s", err) + } + + statement, err := findLambdaPolicyStatementById(&policy, d.Id()) + if err != nil { + return err + } + + qualifier, err := getQualifierFromLambdaAliasOrVersionArn(statement.Resource) + if err == nil { + d.Set("qualifier", qualifier) + } + + // Save Lambda function name in the same format + if strings.HasPrefix(d.Get("function_name").(string), "arn:aws:lambda:") { + // Strip qualifier off + trimmedArn := strings.TrimSuffix(statement.Resource, ":"+qualifier) + d.Set("function_name", trimmedArn) + } else { + functionName, err := getFunctionNameFromLambdaArn(statement.Resource) + if err != nil { + return err + } + d.Set("function_name", functionName) + } + + d.Set("action", statement.Action) + d.Set("principal", statement.Principal["Service"]) + + if stringEquals, ok := statement.Condition["StringEquals"]; ok { + d.Set("source_account", stringEquals["AWS:SourceAccount"]) + } + + if arnLike, ok := statement.Condition["ArnLike"]; ok { + d.Set("source_arn", arnLike["AWS:SourceArn"]) + } + + return nil +} + +func resourceAwsLambdaPermissionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).lambdaconn + + input := lambda.RemovePermissionInput{ + FunctionName: aws.String(d.Get("function_name").(string)), + StatementId: aws.String(d.Id()), + } + + if v, ok := d.GetOk("qualifier"); ok { + input.Qualifier = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Removing Lambda permission: %s", input) + _, err := conn.RemovePermission(&input) + if err != nil { + return err + } + + log.Printf("[DEBUG] Lambda permission with ID %q removed", d.Id()) + d.SetId("") + + return nil +} + +func findLambdaPolicyStatementById(policy *LambdaPolicy, id string) ( + *LambdaPolicyStatement, error) { + + log.Printf("[DEBUG] Received %d statements in Lambda policy", len(policy.Statement)) + for _, statement := range policy.Statement { + if statement.Sid == id { + return &statement, nil + } + } + + return nil, fmt.Errorf("Failed to find statement %q in Lambda policy", id) +} + +func getQualifierFromLambdaAliasOrVersionArn(arn string) (string, error) { + matches := regexp.MustCompile(LambdaFunctionRegexp).FindStringSubmatch(arn) + if len(matches) < 8 || matches[7] == "" { + return "", fmt.Errorf("Invalid ARN or otherwise unable to get qualifier from ARN (%q)", + arn) + } + + return matches[7], nil +} + +func getFunctionNameFromLambdaArn(arn string) (string, error) { + matches := regexp.MustCompile(LambdaFunctionRegexp).FindStringSubmatch(arn) + if len(matches) < 6 || matches[5] == "" { + return "", fmt.Errorf("Invalid ARN or otherwise unable to get qualifier from ARN (%q)", + arn) + } + return matches[5], nil +} + +type LambdaPolicy struct { + Version string + Statement []LambdaPolicyStatement + Id string +} + +type LambdaPolicyStatement struct { + Condition map[string]map[string]string + Action string + Resource string + Effect string + Principal map[string]string + Sid string +} diff --git a/builtin/providers/aws/validators.go b/builtin/providers/aws/validators.go index faa07d82d..101ab8bd1 100644 --- a/builtin/providers/aws/validators.go +++ b/builtin/providers/aws/validators.go @@ -183,3 +183,98 @@ func validateCloudWatchEventTargetId(v interface{}, k string) (ws []string, erro return } + +func validateLambdaFunctionName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 140 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 140 characters: %q", k, value)) + } + // http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + pattern := `^(arn:aws:lambda:)?([a-z]{2}-[a-z]+-\d{1}:)?(\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\$LATEST|[a-zA-Z0-9-_]+))?$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't comply with restrictions (%q): %q", + k, pattern, value)) + } + + return +} + +func validateLambdaQualifier(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 128 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 128 characters: %q", k, value)) + } + // http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + pattern := `^[a-zA-Z0-9$_]+$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't comply with restrictions (%q): %q", + k, pattern, value)) + } + + return +} + +func validateLambdaPermissionAction(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + // http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + pattern := `^(lambda:[*]|lambda:[a-zA-Z]+|[*])$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't comply with restrictions (%q): %q", + k, pattern, value)) + } + + return +} + +func validateAwsAccountId(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + // http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + pattern := `^\d{12}$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't look like AWS Account ID (exactly 12 digits): %q", + k, value)) + } + + return +} + +func validateArn(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + // http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + pattern := `^arn:aws:([a-zA-Z0-9\-])+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:(.*)$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't look like a valid ARN (%q): %q", + k, pattern, value)) + } + + return +} + +func validatePolicyStatementId(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if len(value) > 100 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 100 characters: %q", k, value)) + } + + // http://docs.aws.amazon.com/lambda/latest/dg/API_AddPermission.html + pattern := `^[a-zA-Z0-9-_]+$` + if !regexp.MustCompile(pattern).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q doesn't look like a valid statement ID (%q): %q", + k, pattern, value)) + } + + return +}