diff --git a/.changelog/38307.txt b/.changelog/38307.txt new file mode 100644 index 00000000000..ae4782e9a5a --- /dev/null +++ b/.changelog/38307.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_service_principal +``` \ No newline at end of file diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index dd34122729f..6bdea4aa619 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -460,7 +460,7 @@ service/mediatailor: service/memorydb: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_memorydb_' service/meta: - - '((\*|-)\s*`?|(data|resource)\s+"?)aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service)$' + - '((\*|-)\s*`?|(data|resource)\s+"?)aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service|service_principal)$' service/mgh: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_mgh_' service/mgn: diff --git a/.github/labeler-pr-triage.yml b/.github/labeler-pr-triage.yml index 9b7bed4fdc3..82b133d3f74 100644 --- a/.github/labeler-pr-triage.yml +++ b/.github/labeler-pr-triage.yml @@ -1460,6 +1460,7 @@ service/meta: - 'website/**/partition*' - 'website/**/region*' - 'website/**/service\.*' + - 'website/**/service_principal*' service/mgh: - any: - changed-files: diff --git a/internal/service/meta/service_package_gen.go b/internal/service/meta/service_package_gen.go index 2abe76c7625..60399ce195f 100644 --- a/internal/service/meta/service_package_gen.go +++ b/internal/service/meta/service_package_gen.go @@ -38,6 +38,9 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv { Factory: newDataSourceService, }, + { + Factory: newServicePrincipalDataSource, + }, } } diff --git a/internal/service/meta/service_principal_data_source.go b/internal/service/meta/service_principal_data_source.go new file mode 100644 index 00000000000..69405791f8e --- /dev/null +++ b/internal/service/meta/service_principal_data_source.go @@ -0,0 +1,113 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package meta + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws/endpoints" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkDataSource +func newServicePrincipalDataSource(context.Context) (datasource.DataSourceWithConfigure, error) { + d := &servicePrincipalDataSource{} + + return d, nil +} + +type servicePrincipalDataSource struct { + framework.DataSourceWithConfigure +} + +func (*servicePrincipalDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nosemgrep:ci.meta-in-func-name + response.TypeName = "aws_service_principal" +} + +func (d *servicePrincipalDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrID: schema.StringAttribute{ + Computed: true, + }, + names.AttrName: schema.StringAttribute{ + Computed: true, + }, + names.AttrRegion: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + names.AttrServiceName: schema.StringAttribute{ + Required: true, + }, + "suffix": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (d *servicePrincipalDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var data servicePrincipalDataSourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + var region *endpoints.Region + + // find the region given by the user + if !data.Region.IsNull() { + matchingRegion, err := FindRegionByName(data.Region.ValueString()) + + if err != nil { + response.Diagnostics.AddError("finding Region by name", err.Error()) + + return + } + + region = matchingRegion + } + + // Default to provider current region if no other filters matched + if region == nil { + matchingRegion, err := FindRegionByName(d.Meta().Region) + + if err != nil { + response.Diagnostics.AddError("finding Region using the provider", err.Error()) + + return + } + + region = matchingRegion + } + + partition := names.PartitionForRegion(region.ID()) + + serviceName := "" + + if !data.ServiceName.IsNull() { + serviceName = data.ServiceName.ValueString() + } + + SourceServicePrincipal := names.ServicePrincipalNameForPartition(serviceName, partition) + + data.ID = types.StringValue(serviceName + "." + region.ID() + "." + SourceServicePrincipal) + data.Name = types.StringValue(serviceName + "." + SourceServicePrincipal) + data.Suffix = types.StringValue(SourceServicePrincipal) + data.Region = types.StringValue(region.ID()) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +type servicePrincipalDataSourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Region types.String `tfsdk:"region"` + ServiceName types.String `tfsdk:"service_name"` + Suffix types.String `tfsdk:"suffix"` +} diff --git a/internal/service/meta/service_principal_data_source_test.go b/internal/service/meta/service_principal_data_source_test.go new file mode 100644 index 00000000000..b8431f54845 --- /dev/null +++ b/internal/service/meta/service_principal_data_source_test.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package meta_test + +import ( + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfmeta "github.com/hashicorp/terraform-provider-aws/internal/service/meta" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccMetaServicePrincipal_basic(t *testing.T) { + ctx := acctest.Context(t) + dataSourceName := "data.aws_service_principal.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSPNDataSourceConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, names.AttrID, "s3."+acctest.Region()+".amazonaws.com"), + resource.TestCheckResourceAttr(dataSourceName, names.AttrName, "s3.amazonaws.com"), + resource.TestCheckResourceAttr(dataSourceName, "suffix", "amazonaws.com"), + resource.TestCheckResourceAttr(dataSourceName, names.AttrRegion, acctest.Region()), + resource.TestCheckResourceAttr(dataSourceName, names.AttrServiceName, "s3"), + ), + }, + }, + }) +} + +func TestAccMetaServicePrincipal_MissingService(t *testing.T) { + ctx := acctest.Context(t) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSPNDataSourceConfig_empty, + ExpectError: regexache.MustCompile(`The argument "service_name" is required, but no definition was found.`), + }, + }, + }) +} + +func TestAccMetaServicePrincipal_ByRegion(t *testing.T) { + ctx := acctest.Context(t) + + dataSourceName := "data.aws_service_principal.test" + regions := []string{"us-east-1", "cn-north-1", "us-gov-east-1", "us-iso-east-1", "us-isob-east-1", "eu-isoe-west-1"} //lintignore:AWSAT003 + + for _, region := range regions { + t.Run(region, func(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSPNDataSourceConfig_withRegion("s3", region), + Check: resource.ComposeTestCheckFunc( + //lintignore:AWSR001 + resource.TestCheckResourceAttr(dataSourceName, names.AttrID, fmt.Sprintf("s3.%s.amazonaws.com", region)), + resource.TestCheckResourceAttr(dataSourceName, names.AttrName, "s3.amazonaws.com"), + resource.TestCheckResourceAttr(dataSourceName, "suffix", "amazonaws.com"), + resource.TestCheckResourceAttr(dataSourceName, names.AttrRegion, region), + ), + }, + }, + }) + }) + } +} + +func TestAccMetaServicePrincipal_UniqueForServiceInRegion(t *testing.T) { + ctx := acctest.Context(t) + dataSourceName := "data.aws_service_principal.test" + + type spnTestCase struct { + Service string + Region string + Suffix string + ID string + SPN string + } + + var testCases []spnTestCase + + var regionUniqueServices = []struct { + Region string + Suffix string + Services []string + }{ + { + Region: "us-iso-east-1", //lintignore:AWSAT003 + Suffix: "c2s.ic.gov", + Services: []string{"cloudhsm", "config", "logs", "workspaces"}, + }, + { + Region: "us-isob-east-1", //lintignore:AWSAT003 + Suffix: "sc2s.sgov.gov", + Services: []string{"dms", "logs"}, + }, + { + Region: "cn-north-1", //lintignore:AWSAT003 + Suffix: "amazonaws.com.cn", + Services: []string{"codedeploy", "elasticmapreduce", "logs"}, + }, + } + + for _, region := range regionUniqueServices { + for _, service := range region.Services { + testCases = append(testCases, spnTestCase{ + Service: service, + Region: region.Region, + Suffix: region.Suffix, + ID: fmt.Sprintf("%s.%s.%s", service, region.Region, region.Suffix), + SPN: fmt.Sprintf("%s.%s", service, region.Suffix), + }) + } + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("%s/%s", testCase.Region, testCase.Service), func(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, tfmeta.PseudoServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccSPNDataSourceConfig_withRegion(testCase.Service, testCase.Region), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, names.AttrID, testCase.ID), + resource.TestCheckResourceAttr(dataSourceName, names.AttrName, testCase.SPN), + resource.TestCheckResourceAttr(dataSourceName, "suffix", testCase.Suffix), + resource.TestCheckResourceAttr(dataSourceName, names.AttrRegion, testCase.Region), + ), + }, + }, + }) + }) + } +} + +const testAccSPNDataSourceConfig_empty = ` +data "aws_service_principal" "test" {} +` +const testAccSPNDataSourceConfig_basic = ` +data "aws_service_principal" "test" { + service_name = "s3" +} +` + +func testAccSPNDataSourceConfig_withRegion(service string, region string) string { + return fmt.Sprintf(` +data "aws_service_principal" "test" { + region = %[1]q + service_name = %[2]q +} +`, region, service) +} diff --git a/names/data/names_data.hcl b/names/data/names_data.hcl index c4d3d2ccb54..c885d448a46 100644 --- a/names/data/names_data.hcl +++ b/names/data/names_data.hcl @@ -6130,12 +6130,12 @@ service "meta" { } resource_prefix { - actual = "aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service)$" + actual = "aws_(arn|billing_service_account|default_tags|ip_ranges|partition|regions?|service|service_principal)$" correct = "aws_meta_" } provider_package_correct = "meta" - doc_prefix = ["arn", "ip_ranges", "billing_service_account", "default_tags", "partition", "region", "service\\."] + doc_prefix = ["arn", "ip_ranges", "billing_service_account", "default_tags", "partition", "region", "service\\.", "service_principal"] brand = "" exclude = true allowed_subcategory = true diff --git a/names/names.go b/names/names.go index 7130270ae3b..b79de2f9069 100644 --- a/names/names.go +++ b/names/names.go @@ -256,6 +256,51 @@ func DNSSuffixForPartition(partition string) string { } } +func ServicePrincipalSuffixForPartition(partition string) string { + switch partition { + case ChinaPartitionID: + return "amazonaws.com.cn" + case ISOPartitionID: + return "c2s.ic.gov" + case ISOBPartitionID: + return "sc2s.sgov.gov" + default: + return "amazonaws.com" + } +} + +// SPN region unique taken from +// https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/region-info/lib/default.ts +func ServicePrincipalNameForPartition(service string, partition string) string { + if service != "" && partition != StandardPartitionID { + switch partition { + case ISOPartitionID: + switch service { + case "cloudhsm", + "config", + "logs", + "workspaces": + return DNSSuffixForPartition(partition) + } + case ISOBPartitionID: + switch service { + case "dms", + "logs": + return DNSSuffixForPartition(partition) + } + case ChinaPartitionID: + switch service { + case "codedeploy", + "elasticmapreduce", + "logs": + return DNSSuffixForPartition(partition) + } + } + } + + return "amazonaws.com" +} + func IsOptInRegion(region string) bool { switch region { case AFSouth1RegionID, diff --git a/website/docs/d/service_principal.html.markdown b/website/docs/d/service_principal.html.markdown new file mode 100644 index 00000000000..46ba9caaf75 --- /dev/null +++ b/website/docs/d/service_principal.html.markdown @@ -0,0 +1,37 @@ +--- +subcategory: "Meta Data Sources" +layout: "aws" +page_title: "AWS: aws_service_principal" +description: |- + Compose a Service Principal Name. +--- + +# Data Source: aws_service_principal + +Use this data source to create a Service Principal Name for a service in a given region. Service Principal Names should always end in the standard global format: `{servicename}.amazonaws.com`. However, in some AWS partitions, AWS may expect a different format. + +## Example Usage + +```terraform +data "aws_service_principal" "current" { + service_name = "s3" +} + +data "aws_service_principal" "test" { + service_name = "s3" + region = "us-iso-east-1" +} +``` + +## Argument Reference + +* `service_name` - (Required) Name of the service you want to generate a Service Principal Name for. +* `region` - (Optional) Region you'd like the SPN for. By default, uses the current region. + +## Attribute Reference + +* `id` - Identifier of the current Service Principal (compound of service, region and suffix). (e.g. `logs.us-east-1.amazonaws.com`in AWS Commercial, `logs.cn-north-1.amazonaws.com.cn` in AWS China). +* `name` - Service Principal Name (e.g., `logs.amazonaws.com` in AWS Commercial, `logs.amazonaws.com.cn` in AWS China). +* `service` - Service used for SPN generation (e.g. `logs`). +* `suffix` - Suffix of the SPN (e.g., `amazonaws.com` in AWS Commercial, `amazonaws.com.cn` in AWS China). +*`region` - Region identifier of the generated SPN (e.g., `us-east-1` in AWS Commercial, `cn-north-1` in AWS China).