From 081a61e9349a4d895d5024457d5ba132c1e09ba3 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 8 Aug 2024 13:03:50 -0700 Subject: [PATCH 1/3] Add resource 'platform_saml_settings' --- docs/index.md | 2 +- docs/resources/saml_settings.md | 68 +++ .../platform_saml_settings/import.sh | 1 + .../platform_saml_settings/resource.tf | 17 + pkg/platform/provider.go | 1 + pkg/platform/resource_saml_settings.go | 411 ++++++++++++++++++ pkg/platform/resource_saml_settings_test.go | 137 ++++++ 7 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 docs/resources/saml_settings.md create mode 100644 examples/resources/platform_saml_settings/import.sh create mode 100644 examples/resources/platform_saml_settings/resource.tf create mode 100644 pkg/platform/resource_saml_settings.go create mode 100644 pkg/platform/resource_saml_settings_test.go diff --git a/docs/index.md b/docs/index.md index 47b97be..ed2e2d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -170,6 +170,6 @@ provider "platform" { - `access_token` (String, Sensitive) This is a access token that can be given to you by your admin under `Platform Configuration -> User Management -> Access Tokens`. This can also be sourced from the `JFROG_ACCESS_TOKEN` environment variable. - `check_license` (Boolean, Deprecated) Toggle for pre-flight checking of Artifactory Pro and Enterprise license. Default to `true`. -- `myjfrog_api_token` (String, Sensitive) MyJFrog API token that allows you to make changes to your JFrog account. See [Generate a Token in MyJFrog](https://jfrog.com/help/r/jfrog-hosting-models-documentation/generate-a-token-in-myjfrog) for more details. This can also be sourced from the `JFROG_MYJFROG_API_TOKEN` environment variable. +- `myjfrog_api_token` (String, Sensitive, Deprecated) MyJFrog API token that allows you to make changes to your JFrog account. See [Generate a Token in MyJFrog](https://jfrog.com/help/r/jfrog-hosting-models-documentation/generate-a-token-in-myjfrog) for more details. This can also be sourced from the `JFROG_MYJFROG_API_TOKEN` environment variable. - `oidc_provider_name` (String) OIDC provider name. See [Configure an OIDC Integration](https://jfrog.com/help/r/jfrog-platform-administration-documentation/configure-an-oidc-integration) for more details. - `url` (String) JFrog Platform URL. This can also be sourced from the `JFROG_URL` environment variable. diff --git a/docs/resources/saml_settings.md b/docs/resources/saml_settings.md new file mode 100644 index 0000000..aad3120 --- /dev/null +++ b/docs/resources/saml_settings.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "platform_saml_settings Resource - terraform-provider-platform" +subcategory: "" +description: |- + Provides a JFrog SAML SSO Settings https://jfrog.com/help/r/jfrog-platform-administration-documentation/saml-sso resource. + ~>Only available for self-hosted instances. +--- + +# platform_saml_settings (Resource) + +Provides a JFrog [SAML SSO Settings](https://jfrog.com/help/r/jfrog-platform-administration-documentation/saml-sso) resource. + +~>Only available for self-hosted instances. + +## Example Usage + +```terraform +resource "platform_saml_settings" "my-okta-saml-settings" { + name = "my-okta-saml-settings" + enable = true + certificate = "MIICTjCCA...gPRXbm49Mz4o1nbwH" + email_attribute = "email" + group_attribute = "group" + name_id_attribute = "id" + login_url = "http://tempurl.org/saml" + logout_url = "https://myaccount.okta.com" + no_auto_user_creation = false + service_provider_name = "okta" + allow_user_to_access_profile = true + auto_redirect = true + sync_groups = true + verify_audience_restriction = true + use_encrypted_assertion = false +} +``` + + +## Schema + +### Required + +- `certificate` (String) The certificate for SAML Authentication in Base64 format. NOTE! The certificate must contain the public key to allow Artifactory to verify sign-in requests. +- `login_url` (String) The identity provider login URL (when you try to login, the service provider redirects to this URL). +- `logout_url` (String) The identity provider logout URL (when you try to logout, the service provider redirects to this URL). +- `name` (String) SAML Settings name. +- `service_provider_name` (String) The SAML service provider name. This should be a URI that is also known as the entityID, providerID, or entity identity. + +### Optional + +- `allow_user_to_access_profile` (Boolean) When set, auto created users will have access to their profile page and will be able to perform actions such as generating an API key. Default value is `false`. +- `auto_redirect` (Boolean) When set, clicking on the login link will direct users to the configured SAML login URL. Default value is `false`. +- `email_attribute` (String) If `no_auto_user_creation` is diabled or an internal user exists, the system will set the user's email to the value in this attribute that is returned by the SAML login XML response.. +- `enable` (Boolean) When set, SAML integration is enabled and users may be authenticated via a SAML server. Default value is `true`. +- `group_attribute` (String) The group attribute in the SAML login XML response. Note that the system will search for a case-sensitive match to an existing group.. +- `name_id_attribute` (String) The username attribute used to configure the SSO URL for the identity provider. +- `no_auto_user_creation` (Boolean) When disabled, the system will automatically create new users for those who have logged in using SAML, and assign them to the default groups. Default value is `false`. +- `sync_groups` (Boolean) When set, in addition to the groups the user is already associated with, he will also be associated with the groups returned in the SAML login response. Note that the user's association with the returned groups is not persistent. It is only valid for the current login session. Default value is `false`. +- `use_encrypted_assertion` (Boolean) When set, an X.509 public certificate will be created by Artifactory. Download this certificate and upload it to your IDP and choose your own encryption algorithm. This process will let you encrypt the assertion section in your SAML response. Default value is `false`. +- `verify_audience_restriction` (Boolean) Set this flag to specify who the assertion is intended for. The "audience" will be the service provider and is typically a URL but can technically be formatted as any string of data. Default value is `true`. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import platform_saml_settings.my-okta-saml-settings my-okta-saml-settings +``` diff --git a/examples/resources/platform_saml_settings/import.sh b/examples/resources/platform_saml_settings/import.sh new file mode 100644 index 0000000..405e092 --- /dev/null +++ b/examples/resources/platform_saml_settings/import.sh @@ -0,0 +1 @@ +terraform import platform_saml_settings.my-okta-saml-settings my-okta-saml-settings \ No newline at end of file diff --git a/examples/resources/platform_saml_settings/resource.tf b/examples/resources/platform_saml_settings/resource.tf new file mode 100644 index 0000000..e693310 --- /dev/null +++ b/examples/resources/platform_saml_settings/resource.tf @@ -0,0 +1,17 @@ +resource "platform_saml_settings" "my-okta-saml-settings" { + name = "my-okta-saml-settings" + enable = true + certificate = "MIICTjCCA...gPRXbm49Mz4o1nbwH" + email_attribute = "email" + group_attribute = "group" + name_id_attribute = "id" + login_url = "http://tempurl.org/saml" + logout_url = "https://myaccount.okta.com" + no_auto_user_creation = false + service_provider_name = "okta" + allow_user_to_access_profile = true + auto_redirect = true + sync_groups = true + verify_audience_restriction = true + use_encrypted_assertion = false +} \ No newline at end of file diff --git a/pkg/platform/provider.go b/pkg/platform/provider.go index 1bf05b7..aa85466 100644 --- a/pkg/platform/provider.go +++ b/pkg/platform/provider.go @@ -209,6 +209,7 @@ func (p *PlatformProvider) Resources(ctx context.Context) []func() resource.Reso NewMyJFrogIPAllowListResource, NewPermissionResource, NewReverseProxyResource, + NewSAMLSettingsResource, NewWorkerServiceResource, } } diff --git a/pkg/platform/resource_saml_settings.go b/pkg/platform/resource_saml_settings.go new file mode 100644 index 0000000..b1ccb2a --- /dev/null +++ b/pkg/platform/resource_saml_settings.go @@ -0,0 +1,411 @@ +package platform + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" +) + +const ( + SAMLSettingsEndpoint = "access/api/v1/saml" + SAMLSettingEndpoint = "access/api/v1/saml/{name}" +) + +func NewSAMLSettingsResource() resource.Resource { + return &SAMLSettingsResource{ + TypeName: "platform_saml_settings", + } +} + +type SAMLSettingsResource struct { + ProviderData PlatformProviderMetadata + TypeName string +} + +type SAMLSettingsResourceModel struct { + Name types.String `tfsdk:"name"` + Enable types.Bool `tfsdk:"enable"` + Certificate types.String `tfsdk:"certificate"` + EmailAttribute types.String `tfsdk:"email_attribute"` + GroupAttribute types.String `tfsdk:"group_attribute"` + NameIDAttribute types.String `tfsdk:"name_id_attribute"` + LoginURL types.String `tfsdk:"login_url"` + LogoutURL types.String `tfsdk:"logout_url"` + NoAutoUserCreation types.Bool `tfsdk:"no_auto_user_creation"` + ServiceProviderName types.String `tfsdk:"service_provider_name"` + AllowUserToAccessProfile types.Bool `tfsdk:"allow_user_to_access_profile"` + AutoRedirect types.Bool `tfsdk:"auto_redirect"` + SyncGroups types.Bool `tfsdk:"sync_groups"` + VerifyAudienceRestriction types.Bool `tfsdk:"verify_audience_restriction"` + UseEncryptedAssertion types.Bool `tfsdk:"use_encrypted_assertion"` +} + +func (r *SAMLSettingsResourceModel) toAPIModel(_ context.Context, apiModel *SAMLSettingsAPIModel) diag.Diagnostics { + + apiModel.Name = r.Name.ValueString() + apiModel.Enable = r.Enable.ValueBool() + apiModel.Certificate = r.Certificate.ValueString() + apiModel.EmailAttribute = r.EmailAttribute.ValueString() + apiModel.GroupAttribute = r.GroupAttribute.ValueString() + apiModel.NameIDAttribute = r.NameIDAttribute.ValueString() + apiModel.LoginURL = r.LoginURL.ValueString() + apiModel.LogoutURL = r.LogoutURL.ValueString() + apiModel.NoAutoUserCreation = r.NoAutoUserCreation.ValueBool() + apiModel.ServiceProviderName = r.ServiceProviderName.ValueString() + apiModel.AllowUserToAccessProfile = r.AllowUserToAccessProfile.ValueBool() + apiModel.AutoRedirect = r.AutoRedirect.ValueBool() + apiModel.SyncGroups = r.SyncGroups.ValueBool() + apiModel.VerifyAudienceRestriction = r.VerifyAudienceRestriction.ValueBool() + apiModel.UseEncryptedAssertion = r.UseEncryptedAssertion.ValueBool() + + return nil +} + +func (r *SAMLSettingsResourceModel) fromAPIModel(_ context.Context, apiModel *SAMLSettingsAPIModel) (ds diag.Diagnostics) { + r.Name = types.StringValue(apiModel.Name) + r.Enable = types.BoolValue(apiModel.Enable) + r.Certificate = types.StringValue(apiModel.Certificate) + + if len(apiModel.EmailAttribute) > 0 { + r.EmailAttribute = types.StringValue(apiModel.EmailAttribute) + } + + if len(apiModel.GroupAttribute) > 0 { + r.GroupAttribute = types.StringValue(apiModel.GroupAttribute) + } + + if len(apiModel.NameIDAttribute) > 0 { + r.NameIDAttribute = types.StringValue(apiModel.NameIDAttribute) + } + + r.LoginURL = types.StringValue(apiModel.LoginURL) + r.LogoutURL = types.StringValue(apiModel.LogoutURL) + r.NoAutoUserCreation = types.BoolValue(apiModel.NoAutoUserCreation) + r.ServiceProviderName = types.StringValue(apiModel.ServiceProviderName) + r.AllowUserToAccessProfile = types.BoolValue(apiModel.AllowUserToAccessProfile) + r.AutoRedirect = types.BoolValue(apiModel.AutoRedirect) + r.SyncGroups = types.BoolValue(apiModel.SyncGroups) + r.VerifyAudienceRestriction = types.BoolValue(apiModel.VerifyAudienceRestriction) + r.UseEncryptedAssertion = types.BoolValue(apiModel.UseEncryptedAssertion) + + return +} + +type SAMLSettingsAPIModel struct { + Name string `json:"name"` + Enable bool `json:"enable_integration"` + Certificate string `json:"certificate"` + EmailAttribute string `json:"email_attribute"` + GroupAttribute string `json:"group_attribute"` + NameIDAttribute string `json:"name_id_attribute"` + LoginURL string `json:"login_url"` + LogoutURL string `json:"logout_url"` + NoAutoUserCreation bool `json:"no_auto_user_creation"` + ServiceProviderName string `json:"service_provider_name"` + AllowUserToAccessProfile bool `json:"allow_user_to_access_profile"` + AutoRedirect bool `json:"auto_redirect"` + SyncGroups bool `json:"sync_groups"` + VerifyAudienceRestriction bool `json:"verify_audience_restriction"` + UseEncryptedAssertion bool `json:"use_encrypted_assertion"` +} + +func (r *SAMLSettingsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +func (r *SAMLSettingsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "SAML Settings name.", + }, + "enable": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + MarkdownDescription: "When set, SAML integration is enabled and users may be authenticated via a SAML server. Default value is `true`.", + }, + "certificate": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + MarkdownDescription: "The certificate for SAML Authentication in Base64 format. NOTE! The certificate must contain the public key to allow Artifactory to verify sign-in requests.", + }, + "email_attribute": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + MarkdownDescription: "If `no_auto_user_creation` is diabled or an internal user exists, the system will set the user's email to the value in this attribute that is returned by the SAML login XML response..", + }, + "group_attribute": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + MarkdownDescription: "The group attribute in the SAML login XML response. Note that the system will search for a case-sensitive match to an existing group..", + }, + "name_id_attribute": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + MarkdownDescription: "The username attribute used to configure the SSO URL for the identity provider.", + }, + "login_url": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + validatorfw_string.IsURLHttpOrHttps(), + }, + Description: "The identity provider login URL (when you try to login, the service provider redirects to this URL).", + }, + "logout_url": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + validatorfw_string.IsURLHttpOrHttps(), + }, + Description: "The identity provider logout URL (when you try to logout, the service provider redirects to this URL).", + }, + "no_auto_user_creation": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "When disabled, the system will automatically create new users for those who have logged in using SAML, and assign them to the default groups. Default value is `false`.", + }, + "service_provider_name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + MarkdownDescription: "The SAML service provider name. This should be a URI that is also known as the entityID, providerID, or entity identity.", + }, + "allow_user_to_access_profile": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + MarkdownDescription: "When set, auto created users will have access to their profile page and will be able to perform actions such as generating an API key. Default value is `false`.", + }, + "auto_redirect": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "When set, clicking on the login link will direct users to the configured SAML login URL. Default value is `false`.", + }, + "sync_groups": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "When set, in addition to the groups the user is already associated with, he will also be associated with the groups returned in the SAML login response. Note that the user's association with the returned groups is not persistent. It is only valid for the current login session. Default value is `false`.", + }, + "verify_audience_restriction": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + MarkdownDescription: "Set this flag to specify who the assertion is intended for. The \"audience\" will be the service provider and is typically a URL but can technically be formatted as any string of data. Default value is `true`.", + }, + "use_encrypted_assertion": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "When set, an X.509 public certificate will be created by Artifactory. Download this certificate and upload it to your IDP and choose your own encryption algorithm. This process will let you encrypt the assertion section in your SAML response. Default value is `false`.", + }, + }, + MarkdownDescription: "Provides a JFrog [SAML SSO Settings](https://jfrog.com/help/r/jfrog-platform-administration-documentation/saml-sso) resource.\n\n~>Only available for self-hosted instances.", + } +} + +func (r *SAMLSettingsResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + r.ProviderData = req.ProviderData.(PlatformProviderMetadata) + + supported, err := util.CheckVersion(r.ProviderData.ArtifactoryVersion, "7.83.1") + if err != nil { + resp.Diagnostics.AddError( + "Failed to check Artifactory version", + err.Error(), + ) + return + } + + if !supported { + resp.Diagnostics.AddError( + "Unsupported Artifactory version", + fmt.Sprintf("This resource is supported by Artifactory version 7.83.1 or later. Current version: %s", r.ProviderData.ArtifactoryVersion), + ) + return + } +} + +func (r *SAMLSettingsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan SAMLSettingsResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var samlSettings SAMLSettingsAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &samlSettings)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.ProviderData.Client.R(). + SetBody(samlSettings). + Post(SAMLSettingsEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *SAMLSettingsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state SAMLSettingsResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var samlSettings SAMLSettingsAPIModel + + response, err := r.ProviderData.Client.R(). + SetPathParam("name", state.Name.ValueString()). + SetResult(&samlSettings). + Get(SAMLSettingEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + // Treat HTTP 404 Not Found status as a signal to recreate resource + // and return early + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, response.String()) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(state.fromAPIModel(ctx, &samlSettings)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *SAMLSettingsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan SAMLSettingsResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var samlSettings SAMLSettingsAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &samlSettings)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.ProviderData.Client.R(). + SetPathParam("name", plan.Name.ValueString()). + SetBody(samlSettings). + Put(SAMLSettingEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *SAMLSettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state SAMLSettingsResourceModel + + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + response, err := r.ProviderData.Client.R(). + SetPathParam("name", state.Name.ValueString()). + Delete(SAMLSettingEndpoint) + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, response.String()) + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +func (r *SAMLSettingsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) +} diff --git a/pkg/platform/resource_saml_settings_test.go b/pkg/platform/resource_saml_settings_test.go new file mode 100644 index 0000000..757d38b --- /dev/null +++ b/pkg/platform/resource_saml_settings_test.go @@ -0,0 +1,137 @@ +package platform_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/jfrog/terraform-provider-platform/pkg/platform" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccSAMLSettings_full(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-saml-settings", "platform_saml_settings") + + temp := ` + resource "platform_saml_settings" "{{ .name }}" { + name = "{{ .name }}" + enable = true + certificate = "{{ .certificate }}" + email_attribute = "{{ .email_attribute }}" + group_attribute = "{{ .group_attribute }}" + name_id_attribute = "{{ .name_id_attribute }}" + login_url = "http://tempurl.org/login" + logout_url = "http://tempurl.org/logout" + no_auto_user_creation = false + service_provider_name = "okta" + allow_user_to_access_profile = true + auto_redirect = true + sync_groups = true + verify_audience_restriction = true + use_encrypted_assertion = false + }` + + testData := map[string]string{ + "name": name, + "certificate": "MIICTjCCAbegAwIBAgIBADANBgkqhkiG9w0BAQ0FADBEMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExFjAUBgNVBAoMDUpGcm9nIFRlc3RpbmcxEDAOBgNVBAMMB1Rlc3RpbmcwHhcNMjQwODA4MTgzNjMxWhcNMjUwODA4MTgzNjMxWjBEMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExFjAUBgNVBAoMDUpGcm9nIFRlc3RpbmcxEDAOBgNVBAMMB1Rlc3RpbmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOPwKU3SxuRaJply2by60NxYmbIPfelhM6sObgPRXbm49Mz4o1nbwH/vwhz1K+klVO4hOiKc5aP5GtQEoBejZbxOXlYlf8YirNqbtEXlIattvZA3tlC8O9oNOzBuT6tRdAA9CvN035p17fN0tpejz7Ptn1G1yUAt9klTUBBZ8eERAgMBAAGjUDBOMB0GA1UdDgQWBBR2y2SefjbqeSHTj+URrKc540YkGTAfBgNVHSMEGDAWgBR2y2SefjbqeSHTj+URrKc540YkGTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAKxnkFRgLZnQ4U6fWjfuJnx29cKbIq4oBr9RuWEKH2Hhx+jWy/3baNrxE0AsNWTLX6gGVd2qJbfae803AN6ZLx+VrLCWKl+c5MTTZBhuX6G/JvWviavE44P1U4cl2c6w4qvAmY+SY0cnJeWGLCBJ2vJ/fauXS/TIr0IfziSRcVYY", + "email_attribute": "email", + "group_attribute": "group", + "name_id_attribute": "name", + } + + config := util.ExecuteTemplate(name, temp, testData) + + updatedTestData := map[string]string{ + "name": name, + "certificate": "MIICTjCCAbegAwIBAgIBADANBgkqhkiG9w0BAQ0FADBEMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExFjAUBgNVBAoMDUpGcm9nIFRlc3RpbmcxEDAOBgNVBAMMB1Rlc3RpbmcwHhcNMjQwODA4MTgzNjMxWhcNMjUwODA4MTgzNjMxWjBEMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExFjAUBgNVBAoMDUpGcm9nIFRlc3RpbmcxEDAOBgNVBAMMB1Rlc3RpbmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOPwKU3SxuRaJply2by60NxYmbIPfelhM6sObgPRXbm49Mz4o1nbwH/vwhz1K+klVO4hOiKc5aP5GtQEoBejZbxOXlYlf8YirNqbtEXlIattvZA3tlC8O9oNOzBuT6tRdAA9CvN035p17fN0tpejz7Ptn1G1yUAt9klTUBBZ8eERAgMBAAGjUDBOMB0GA1UdDgQWBBR2y2SefjbqeSHTj+URrKc540YkGTAfBgNVHSMEGDAWgBR2y2SefjbqeSHTj+URrKc540YkGTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAKxnkFRgLZnQ4U6fWjfuJnx29cKbIq4oBr9RuWEKH2Hhx+jWy/3baNrxE0AsNWTLX6gGVd2qJbfae803AN6ZLx+VrLCWKl+c5MTTZBhuX6G/JvWviavE44P1U4cl2c6w4qvAmY+SY0cnJeWGLCBJ2vJ/fauXS/TIr0IfziSRcVYY", + "email_attribute": "email2", + "group_attribute": "group2", + "name_id_attribute": "name2", + } + + updatedConfig := util.ExecuteTemplate(name, temp, updatedTestData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + CheckDestroy: testAccSamlSettingsDestroy(fqrn), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", testData["name"]), + resource.TestCheckResourceAttr(fqrn, "enable", "true"), + resource.TestCheckResourceAttr(fqrn, "certificate", testData["certificate"]), + resource.TestCheckResourceAttr(fqrn, "email_attribute", testData["email_attribute"]), + resource.TestCheckResourceAttr(fqrn, "group_attribute", testData["group_attribute"]), + resource.TestCheckResourceAttr(fqrn, "name_id_attribute", testData["name_id_attribute"]), + resource.TestCheckResourceAttr(fqrn, "login_url", "http://tempurl.org/login"), + resource.TestCheckResourceAttr(fqrn, "logout_url", "http://tempurl.org/logout"), + resource.TestCheckResourceAttr(fqrn, "no_auto_user_creation", "false"), + resource.TestCheckResourceAttr(fqrn, "service_provider_name", "okta"), + resource.TestCheckResourceAttr(fqrn, "allow_user_to_access_profile", "true"), + resource.TestCheckResourceAttr(fqrn, "auto_redirect", "true"), + resource.TestCheckResourceAttr(fqrn, "sync_groups", "true"), + resource.TestCheckResourceAttr(fqrn, "verify_audience_restriction", "true"), + resource.TestCheckResourceAttr(fqrn, "use_encrypted_assertion", "false"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", updatedTestData["name"]), + resource.TestCheckResourceAttr(fqrn, "enable", "true"), + resource.TestCheckResourceAttr(fqrn, "certificate", updatedTestData["certificate"]), + resource.TestCheckResourceAttr(fqrn, "email_attribute", updatedTestData["email_attribute"]), + resource.TestCheckResourceAttr(fqrn, "group_attribute", updatedTestData["group_attribute"]), + resource.TestCheckResourceAttr(fqrn, "name_id_attribute", updatedTestData["name_id_attribute"]), + resource.TestCheckResourceAttr(fqrn, "login_url", "http://tempurl.org/login"), + resource.TestCheckResourceAttr(fqrn, "logout_url", "http://tempurl.org/logout"), + resource.TestCheckResourceAttr(fqrn, "no_auto_user_creation", "false"), + resource.TestCheckResourceAttr(fqrn, "service_provider_name", "okta"), + resource.TestCheckResourceAttr(fqrn, "allow_user_to_access_profile", "true"), + resource.TestCheckResourceAttr(fqrn, "auto_redirect", "true"), + resource.TestCheckResourceAttr(fqrn, "sync_groups", "true"), + resource.TestCheckResourceAttr(fqrn, "verify_audience_restriction", "true"), + resource.TestCheckResourceAttr(fqrn, "use_encrypted_assertion", "false"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + ImportStateId: name, + ImportStateVerifyIdentifierAttribute: "name", + }, + }, + }) +} + +func testAccSamlSettingsDestroy(id string) func(*terraform.State) error { + return func(s *terraform.State) error { + c := TestProvider.(*platform.PlatformProvider).Meta.Client + + rs, ok := s.RootModule().Resources[id] + if !ok { + return fmt.Errorf("error: resource id [%s] not found", id) + } + + var samlSettings platform.SAMLSettingsAPIModel + resp, err := c.R(). + SetPathParam("name", rs.Primary.Attributes["name"]). + SetResult(&samlSettings). + Get(platform.SAMLSettingEndpoint) + if err != nil { + return err + } + + if resp != nil && resp.StatusCode() == http.StatusNotFound { + return nil + } + + return fmt.Errorf("error: SAML Settings %s still exists", rs.Primary.Attributes["name"]) + } +} From 3d6513e97df89d65b2486868b696192adbb86e66 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 8 Aug 2024 13:10:35 -0700 Subject: [PATCH 2/3] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e61c54..0ee544c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.11.0 (August 9, 2024) + +FEATURES: + +**New Resource:** `platform_saml_settings` - Resource to manage SAML SSO settings. PR: [#118](https://github.com/jfrog/terraform-provider-platform/pull/118) + ## 1.10.0 (July 21, 2024). Tested on Artifactory 7.84.17 with Terraform 1.9.2 and OpenTofu 1.7.3 NOTES: From 90eb24770f803c978896c1de4ffb39cc03cb90b0 Mon Sep 17 00:00:00 2001 From: JFrog CI Date: Thu, 8 Aug 2024 20:16:26 +0000 Subject: [PATCH 3/3] JFrog Pipelines - Add Artifactory version to CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee544c..76bfad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.11.0 (August 9, 2024) +## 1.11.0 (August 9, 2024). Tested on Artifactory 7.90.7 with Terraform 1.9.4 and OpenTofu 1.8.1 FEATURES: