diff --git a/CHANGELOG.md b/CHANGELOG.md index 502b1fba7..00be9cd5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ nav_order: 1 - Remove `aiven_valkey` from beta resources - Remove `aiven_valkey_user` from beta resources - Adds`aiven_organization_permission` example +- Add capability to map external service user with internal aiven user with external_identity data source ## [4.25.0] - 2024-09-17 diff --git a/docs/data-sources/external_identity.md b/docs/data-sources/external_identity.md new file mode 100644 index 000000000..29fe3a16d --- /dev/null +++ b/docs/data-sources/external_identity.md @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_external_identity Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + Maps an external service user to an Aiven user. +--- + +# aiven_external_identity (Data Source) + +Maps an external service user to an Aiven user. + + + + +## Schema + +### Required + +- `external_service_name` (String) The name of the external service. The possible values are `github`. +- `external_user_id` (String) The user's ID on the external service. +- `internal_user_id` (String) The Aiven user ID. +- `organization_id` (String) The ID of the Aiven organization that the user is part of. diff --git a/internal/plugin/provider.go b/internal/plugin/provider.go index 5e46333ef..3f17213cb 100644 --- a/internal/plugin/provider.go +++ b/internal/plugin/provider.go @@ -13,6 +13,7 @@ import ( "github.com/aiven/terraform-provider-aiven/internal/common" "github.com/aiven/terraform-provider-aiven/internal/plugin/errmsg" + "github.com/aiven/terraform-provider-aiven/internal/plugin/service/externalidentity" "github.com/aiven/terraform-provider-aiven/internal/plugin/service/organization" "github.com/aiven/terraform-provider-aiven/internal/plugin/util" ) @@ -138,7 +139,9 @@ func (p *AivenProvider) DataSources(context.Context) []func() datasource.DataSou // Add to a list of data sources that are currently in beta. if util.IsBeta() { - var betaDataSources []func() datasource.DataSource + betaDataSources := []func() datasource.DataSource{ + externalidentity.NewExternalIdentityDataSource, + } dataSources = append(dataSources, betaDataSources...) } diff --git a/internal/plugin/service/externalidentity/external_identity_data_source.go b/internal/plugin/service/externalidentity/external_identity_data_source.go new file mode 100644 index 000000000..830009bde --- /dev/null +++ b/internal/plugin/service/externalidentity/external_identity_data_source.go @@ -0,0 +1,142 @@ +package externalidentity + +import ( + "context" + "fmt" + + "github.com/aiven/aiven-go-client/v2" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" +) + +var ( + _ datasource.DataSource = &externalIdentityDataSource{} + _ datasource.DataSourceWithConfigure = &externalIdentityDataSource{} + _ util.TypeNameable = &externalIdentityDataSource{} +) + +func NewExternalIdentityDataSource() datasource.DataSource { + return &externalIdentityDataSource{} +} + +type externalIdentityDataSource struct { + client *aiven.Client + typeName string +} + +// externalIdentityDataSourceModel is the model for the external_identity data source. +type externalIdentityDataSourceModel struct { + OrganizationID types.String `tfsdk:"organization_id"` + InternalUserID types.String `tfsdk:"internal_user_id"` + ExternalUserID types.String `tfsdk:"external_user_id"` + ExternalServiceName types.String `tfsdk:"external_service_name"` +} + +// Metadata returns the metadata for the external_identity data source. +func (r *externalIdentityDataSource) Metadata( + _ context.Context, + req datasource.MetadataRequest, + resp *datasource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_external_identity" + r.typeName = resp.TypeName +} + +// TypeName returns the data source type name for the external_identity data source. +func (r *externalIdentityDataSource) TypeName() string { + return r.typeName +} + +// Schema defines the schema for the external_identity data source. +func (r *externalIdentityDataSource) Schema( + _ context.Context, + _ datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Maps an external service user to an Aiven user.", + Attributes: map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + Description: "The ID of the Aiven organization that the user is part of.", + Required: true, + }, + "internal_user_id": schema.StringAttribute{ + Description: "The Aiven user ID.", + Required: true, + }, + "external_user_id": schema.StringAttribute{ + Description: "The user's ID on the external service.", + Required: true, + }, + "external_service_name": schema.StringAttribute{ + Description: userconfig.Desc("The name of the external service.").PossibleValuesString("github").Build(), + Required: true, + }, + }, + } +} + +// Configure sets up the external_identity data source. +func (r *externalIdentityDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*aiven.Client) + if !ok { + resp.Diagnostics = util.DiagErrorUnexpectedProviderDataType(resp.Diagnostics, req.ProviderData) + + return + } + + r.client = client +} + +// Read reads an external_identity data source. +func (r *externalIdentityDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state externalIdentityDataSourceModel + + if !util.ConfigToModel(ctx, &req.Config, &state, &resp.Diagnostics) { + return + } + + organizationID := state.OrganizationID.ValueString() + internalUserID := state.InternalUserID.ValueString() + externalUserID := state.ExternalUserID.ValueString() + externalServiceName := state.ExternalServiceName.ValueString() + + responseData, err := r.client.OrganizationUser.List(ctx, organizationID) + if err != nil { + resp.Diagnostics = util.DiagErrorReadingDataSource(resp.Diagnostics, r, err) + return + } + + var user *aiven.OrganizationMemberInfo + for _, member := range responseData.Users { + if member.UserID == internalUserID { + user = &member + break + } + } + + if user == nil { + err := fmt.Errorf("organization user %s not found in organization %s", internalUserID, organizationID) + resp.Diagnostics = util.DiagErrorReadingDataSource(resp.Diagnostics, r, err) + return + } + + state.OrganizationID = types.StringValue(organizationID) + state.InternalUserID = types.StringValue(internalUserID) + state.ExternalUserID = types.StringValue(externalUserID) + state.ExternalServiceName = types.StringValue(externalServiceName) + + util.ModelToPlanState(ctx, state, &resp.State, &resp.Diagnostics) +} diff --git a/internal/plugin/service/externalidentity/external_identity_data_source_test.go b/internal/plugin/service/externalidentity/external_identity_data_source_test.go new file mode 100644 index 000000000..1d147fd53 --- /dev/null +++ b/internal/plugin/service/externalidentity/external_identity_data_source_test.go @@ -0,0 +1,67 @@ +package externalidentity_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" +) + +// TestExternalIdentityDataSource tests the external_identity datasource. +func TestExternalIdentityDataSource(t *testing.T) { + deps := acc.CommonTestDependencies(t) + + _ = deps.IsBeta(true) + + organizationName := deps.OrganizationName() + userID := deps.OrganizationUserID(true) + prefix := acc.DefaultResourceNamePrefix + suffix := acctest.RandStringFromCharSet(acc.DefaultRandomSuffixLength, acctest.CharSetAlphaNum) + userGroupName := fmt.Sprintf("%s-usr-group-%s", prefix, suffix) + resourceName := "data.aiven_external_identity.foo" + externalUserID := "alice" + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testExternalIdentityDataSourceBasic(organizationName, userGroupName, *userID, externalUserID), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "external_user_id", "alice"), + resource.TestCheckResourceAttr(resourceName, "internal_user_id", *userID), + ), + }, + }, + }) +} + +func testExternalIdentityDataSourceBasic(organizationName string, userGroupName string, userID string, externalUserID string) string { + return fmt.Sprintf(` +data "aiven_organization" "foo" { + name = "%s" +} + +resource "aiven_organization_user_group" "foo" { + organization_id = data.aiven_organization.foo.id + name = "%s" + description = "Terraform acceptance tests" +} + +resource "aiven_organization_user_group_member" "foo" { + organization_id = data.aiven_organization.foo.id + group_id = aiven_organization_user_group.foo.group_id + user_id = "%s" +} + +data "aiven_external_identity" "foo" { + organization_id = data.aiven_organization.foo.id + internal_user_id = "%[3]s" + external_user_id = "%s" + external_service_name = "github" +} +`, organizationName, userGroupName, userID, externalUserID) +}