Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#13994: Attempt to allow usage of service principals for KMS grants. #25360

3 changes: 3 additions & 0 deletions .changelog/25360.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_kms_grant: Allow usage of service principal as grantee and revoker.
```
9 changes: 4 additions & 5 deletions internal/service/kms/grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ func ResourceGrant() *schema.Resource {
},
Set: resourceGrantConstraintsHash,
},

"grant_creation_tokens": {
Type: schema.TypeSet,
Optional: true,
Expand All @@ -90,7 +89,7 @@ func ResourceGrant() *schema.Resource {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: verify.ValidARN,
ValidateFunc: verify.ValidARNOrServicePrincipal,
},
"key_id": {
Type: schema.TypeString,
Expand Down Expand Up @@ -122,7 +121,7 @@ func ResourceGrant() *schema.Resource {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: verify.ValidARN,
ValidateFunc: verify.ValidARNOrServicePrincipal,
},
},
}
Expand Down Expand Up @@ -328,13 +327,13 @@ func findGrantByTwoPartKeyWithRetry(ctx context.Context, conn *kms.KMS, keyID, g
}

if principal := aws.StringValue(grant.GranteePrincipal); principal != "" {
if !arn.IsARN(principal) {
if !arn.IsARN(principal) && !verify.IsServicePrincipal(principal) {
return resource.RetryableError(fmt.Errorf("grantee principal (%s) is invalid. Perhaps the principal has been deleted or recreated", principal))
}
}

if principal := aws.StringValue(grant.RetiringPrincipal); principal != "" {
if !arn.IsARN(principal) {
if !arn.IsARN(principal) && !verify.IsServicePrincipal(principal) {
return resource.RetryableError(fmt.Errorf("retiring principal (%s) is invalid. Perhaps the principal has been deleted or recreated", principal))
}
}
Expand Down
63 changes: 57 additions & 6 deletions internal/service/kms/grant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,41 @@ func TestAccKMSGrant_basic(t *testing.T) {
})
}

func TestAccKMSGrant_service(t *testing.T) {
ctx := acctest.Context(t)
resourceName := "aws_kms_grant.test"
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
servicePrincipal := "dynamodb.us-west-1.amazonaws.com" //lintignore:AWSAT003

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, kms.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckGrantDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccGrantConfig_service(rName, "\"Encrypt\", \"Decrypt\"", servicePrincipal),
Check: resource.ComposeTestCheckFunc(
testAccCheckGrantExists(ctx, resourceName),
resource.TestCheckResourceAttr(resourceName, "name", rName),
resource.TestCheckResourceAttr(resourceName, "grantee_principal", servicePrincipal),
resource.TestCheckResourceAttr(resourceName, "retiring_principal", servicePrincipal),
resource.TestCheckResourceAttr(resourceName, "operations.#", "2"),
resource.TestCheckTypeSetElemAttr(resourceName, "operations.*", "Encrypt"),
resource.TestCheckTypeSetElemAttr(resourceName, "operations.*", "Decrypt"),
resource.TestCheckResourceAttrPair(resourceName, "key_id", "aws_kms_key.test", "key_id"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"grant_token", "retire_on_delete"},
},
},
})
}

func TestAccKMSGrant_withConstraints(t *testing.T) {
ctx := acctest.Context(t)
resourceName := "aws_kms_grant.test"
Expand Down Expand Up @@ -349,17 +384,21 @@ data "aws_iam_policy_document" "test" {
}
}
}
`, rName)
}

func testAccGrantConfig_base_role(rName string) string {
return acctest.ConfigCompose(testAccGrantConfig_base(rName), fmt.Sprintf(`
resource "aws_iam_role" "test" {
name = %[1]q
path = "/service-role/"
assume_role_policy = data.aws_iam_policy_document.test.json
}
`, rName)
`, rName))
}

func testAccGrantConfig_basic(rName string, operations string) string {
return acctest.ConfigCompose(testAccGrantConfig_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccGrantConfig_base_role(rName), fmt.Sprintf(`
resource "aws_kms_grant" "test" {
name = %[1]q
key_id = aws_kms_key.test.key_id
Expand All @@ -369,8 +408,20 @@ resource "aws_kms_grant" "test" {
`, rName, operations))
}

func testAccGrantConfig_constraints(rName string, constraintName string, encryptionContext string) string {
func testAccGrantConfig_service(rName string, operations string, servicePrincipal string) string {
return acctest.ConfigCompose(testAccGrantConfig_base(rName), fmt.Sprintf(`
resource "aws_kms_grant" "test" {
name = %[1]q
key_id = aws_kms_key.test.key_id
operations = [%[2]s]
grantee_principal = %[3]q
retiring_principal = %[3]q
}
`, rName, operations, servicePrincipal))
}

func testAccGrantConfig_constraints(rName string, constraintName string, encryptionContext string) string {
return acctest.ConfigCompose(testAccGrantConfig_base_role(rName), fmt.Sprintf(`
resource "aws_kms_grant" "test" {
name = %[1]q
key_id = aws_kms_key.test.key_id
Expand All @@ -387,7 +438,7 @@ resource "aws_kms_grant" "test" {
}

func testAccGrantConfig_retiringPrincipal(rName string) string {
return acctest.ConfigCompose(testAccGrantConfig_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccGrantConfig_base_role(rName), fmt.Sprintf(`
resource "aws_kms_grant" "test" {
name = %[1]q
key_id = aws_kms_key.test.key_id
Expand All @@ -399,7 +450,7 @@ resource "aws_kms_grant" "test" {
}

func testAccGrantConfig_bare(rName string) string {
return acctest.ConfigCompose(testAccGrantConfig_base(rName), `
return acctest.ConfigCompose(testAccGrantConfig_base_role(rName), `
resource "aws_kms_grant" "test" {
key_id = aws_kms_key.test.key_id
grantee_principal = aws_iam_role.test.arn
Expand All @@ -409,7 +460,7 @@ resource "aws_kms_grant" "test" {
}

func testAccGrantConfig_arn(rName string, operations string) string {
return acctest.ConfigCompose(testAccGrantConfig_base(rName), fmt.Sprintf(`
return acctest.ConfigCompose(testAccGrantConfig_base_role(rName), fmt.Sprintf(`
resource "aws_kms_grant" "test" {
name = %[1]q
key_id = aws_kms_key.test.arn
Expand Down
38 changes: 38 additions & 0 deletions internal/verify/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,44 @@ var accountIDRegexp = regexp.MustCompile(`^(aws|aws-managed|third-party|\d{12})$
var partitionRegexp = regexp.MustCompile(`^aws(-[a-z]+)*$`)
var regionRegexp = regexp.MustCompile(`^[a-z]{2}(-[a-z]+)+-\d$`)

// validates all listed in https://gist.github.com/shortjared/4c1e3fe52bdfa47522cfe5b41e5d6f22
var servicePrincipalRegexp = regexp.MustCompile(`^([a-z0-9-]+\.){1,4}(amazonaws|amazon)\.com$`)

func ValidARNOrServicePrincipal(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

if value == "" {
return ws, errors
}

var _, errors_arn = ValidARN(v, k)
var _, errors_sp = ValidServicePrincipal(v, k)

if len(errors_arn) > 0 && len(errors_sp) > 0 {
errors = []error{fmt.Errorf("%q (%s) is neither a valid service principal or ARN", k, value)}
}

return ws, errors
}

func ValidServicePrincipal(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

if value == "" {
return ws, errors
}

if !IsServicePrincipal(value) {
errors = append(errors, fmt.Errorf("%q (%s) is an invalid Service Principal: invalid prefix value (expecting to match regular expression: %s)", k, value, servicePrincipalRegexp))
}

return ws, errors
}

func IsServicePrincipal(value string) (valid bool) {
return servicePrincipalRegexp.MatchString(value)
}

func Valid4ByteASN(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

Expand Down
77 changes: 77 additions & 0 deletions internal/verify/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,83 @@ func TestValidARN(t *testing.T) {
}
}

func TestValidServicePrincipal(t *testing.T) {
t.Parallel()

v := ""
_, errors := ValidServicePrincipal(v, "test.google.com")
if len(errors) != 0 {
t.Fatalf("%q should not be validated as an Service Principal name: %q", v, errors)
}

validNames := []string{
"a4b.amazonaws.com",
"appstream.application-autoscaling.amazonaws.com",
"alexa-appkit.amazon.com",
"member.org.stacksets.cloudformation.amazonaws.com",
"vpc-flow-logs.amazonaws.com",
"logs.eu-central-1.amazonaws.com",
}
for _, v := range validNames {
_, errors := ValidServicePrincipal(v, "arn")
if len(errors) != 0 {
t.Fatalf("%q should be a valid Service Principal: %q", v, errors)
}
}

invalidNames := []string{
"test.google.com",
"transfer.amz.com",
"test",
"testwithwildcard*",
}
for _, v := range invalidNames {
_, errors := ValidServicePrincipal(v, "arn")
if len(errors) == 0 {
t.Fatalf("%q should be an invalid Service Principal", v)
}
}
}

func TestValidARNOrServicePrincipal(t *testing.T) {
t.Parallel()

v := ""
_, errors := ValidARNOrServicePrincipal(v, "test.google.com")
if len(errors) != 0 {
t.Fatalf("%q should not be validated as an Service Principal name: %q", v, errors)
}

validNames := []string{
"a4b.amazonaws.com",
"appstream.application-autoscaling.amazonaws.com",
"alexa-appkit.amazon.com",
"arn:aws:lambda:eu-west-1:319201112229:function:myCustomFunction:Qualifier", // lintignore:AWSAT003,AWSAT005 // Lambda func qualifier
"arn:aws-cn:ec2:cn-north-1:123456789012:instance/i-12345678", // lintignore:AWSAT003,AWSAT005 // China EC2 ARN
"arn:aws-cn:s3:::bucket/object", // lintignore:AWSAT005 // China S3 ARN
}
for _, v := range validNames {
_, errors := ValidARNOrServicePrincipal(v, "arn")
if len(errors) != 0 {
t.Fatalf("%q should be a valid Service Principal or ARN: %q", v, errors)
}
}

invalidNames := []string{
"test.google.com",
"testwithwildcard*",
"arn",
"123456789012",
"arn:aws",
}
for _, v := range invalidNames {
_, errors := ValidARNOrServicePrincipal(v, "arn")
if len(errors) == 0 {
t.Fatalf("%q should be an invalid Service Principal and ARN", v)
}
}
}

func TestValidateCIDRBlock(t *testing.T) {
t.Parallel()

Expand Down