diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b755a3d..a8af1943c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +FEATURES: +* **New Resource**: `r/tfe_saml_settings` manages SAML Settings, by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970) +* `d/tfe_saml_settings`: Add PrivateKey (sensitive), SignatureSigningMethod, and SignatureDigestMethod attributes, by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970) + +NOTES: +* The provider is now using go-tfe [v1.30.0](https://github.com/hashicorp/go-tfe/releases/tag/v1.30.0), by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970) + ## v0.47.0 (July 18, 2023) FEATURES: diff --git a/tfe/data_source_saml_settings.go b/tfe/data_source_saml_settings.go index e219e0eca..662b5b71e 100644 --- a/tfe/data_source_saml_settings.go +++ b/tfe/data_source_saml_settings.go @@ -49,6 +49,9 @@ type modelTFESAMLSettings struct { ACSConsumerURL types.String `tfsdk:"acs_consumer_url"` MetadataURL types.String `tfsdk:"metadata_url"` Certificate types.String `tfsdk:"certificate"` + PrivateKey types.String `tfsdk:"private_key"` + SignatureSigningMethod types.String `tfsdk:"signature_signing_method"` + SignatureDigestMethod types.String `tfsdk:"signature_digest_method"` } // Metadata returns the data source type name. @@ -114,6 +117,16 @@ func (d *dataSourceTFESAMLSettings) Schema(_ context.Context, _ datasource.Schem "certificate": schema.StringAttribute{ Computed: true, }, + "private_key": schema.StringAttribute{ + Computed: true, + Sensitive: true, + }, + "signature_signing_method": schema.StringAttribute{ + Computed: true, + }, + "signature_digest_method": schema.StringAttribute{ + Computed: true, + }, }, } } @@ -164,6 +177,9 @@ func (d *dataSourceTFESAMLSettings) Read(ctx context.Context, _ datasource.ReadR ACSConsumerURL: types.StringValue(s.ACSConsumerURL), MetadataURL: types.StringValue(s.MetadataURL), Certificate: types.StringValue(s.Certificate), + PrivateKey: types.StringValue(s.PrivateKey), + SignatureSigningMethod: types.StringValue(s.SignatureSigningMethod), + SignatureDigestMethod: types.StringValue(s.SignatureDigestMethod), }) resp.Diagnostics.Append(diags...) } diff --git a/tfe/data_source_saml_settings_test.go b/tfe/data_source_saml_settings_test.go index 111e1d7f5..d859dc9d0 100644 --- a/tfe/data_source_saml_settings_test.go +++ b/tfe/data_source_saml_settings_test.go @@ -4,7 +4,6 @@ package tfe import ( - "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "testing" ) @@ -39,5 +38,5 @@ func TestAccTFESAMLSettingsDataSource_basic(t *testing.T) { } func testAccTFESAMLSettingsDataSourceConfig_basic() string { - return fmt.Sprint(`data "tfe_saml_settings" "foobar" {}`) + return `data "tfe_saml_settings" "foobar" {}` } diff --git a/tfe/provider_next.go b/tfe/provider_next.go index 28cdc07f7..591bd6a97 100644 --- a/tfe/provider_next.go +++ b/tfe/provider_next.go @@ -119,5 +119,6 @@ func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewResourceVariable, + NewSAMLSettingsResource, } } diff --git a/tfe/resource_tfe_saml_settings.go b/tfe/resource_tfe_saml_settings.go new file mode 100644 index 000000000..e03a8301b --- /dev/null +++ b/tfe/resource_tfe_saml_settings.go @@ -0,0 +1,362 @@ +package tfe + +import ( + "context" + "fmt" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +const ( + samlSignatureMethodSHA1 string = "SHA1" + samlSignatureMethodSHA256 string = "SHA256" + samlDefaultAttrUsername string = "Username" + samlDefaultAttrSiteAdmin string = "SiteAdmin" + samlDefaultAttrGroups string = "MemberOf" + samlDefaultSiteAdminRole string = "site-admins" + samlDefaultSSOAPITokenSessionTimeoutSeconds int64 = 1209600 // 14 days +) + +// resourceTFESAMLSettings implements the tfe_saml_settings resource type +type resourceTFESAMLSettings struct { + client *tfe.Client +} + +// modelFromTFEAdminSAMLSettings builds a modelTFESAMLSettings struct from a tfe.AdminSAMLSetting value +func modelFromTFEAdminSAMLSettings(v tfe.AdminSAMLSetting, privateKey types.String) modelTFESAMLSettings { + m := modelTFESAMLSettings{ + ID: types.StringValue(v.ID), + Enabled: types.BoolValue(v.Enabled), + Debug: types.BoolValue(v.Debug), + AuthnRequestsSigned: types.BoolValue(v.AuthnRequestsSigned), + WantAssertionsSigned: types.BoolValue(v.WantAssertionsSigned), + TeamManagementEnabled: types.BoolValue(v.TeamManagementEnabled), + OldIDPCert: types.StringValue(v.OldIDPCert), + IDPCert: types.StringValue(v.IDPCert), + SLOEndpointURL: types.StringValue(v.SLOEndpointURL), + SSOEndpointURL: types.StringValue(v.SSOEndpointURL), + AttrUsername: types.StringValue(v.AttrUsername), + AttrGroups: types.StringValue(v.AttrGroups), + AttrSiteAdmin: types.StringValue(v.AttrSiteAdmin), + SiteAdminRole: types.StringValue(v.SiteAdminRole), + SSOAPITokenSessionTimeout: types.Int64Value(int64(v.SSOAPITokenSessionTimeout)), + ACSConsumerURL: types.StringValue(v.ACSConsumerURL), + MetadataURL: types.StringValue(v.MetadataURL), + Certificate: types.StringValue(v.Certificate), + PrivateKey: types.StringValue(""), + SignatureSigningMethod: types.StringValue(v.SignatureSigningMethod), + SignatureDigestMethod: types.StringValue(v.SignatureDigestMethod), + } + if len(privateKey.String()) > 0 { + m.PrivateKey = privateKey + } + return m +} + +// Configure implements resource.ResourceWithConfigure +func (r *resourceTFESAMLSettings) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Early exit if provider is not properly configured (i.e. we're only validating config or something) + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(ConfiguredClient) + if !ok { + resp.Diagnostics.AddError( + "Unexpected resource Configure type", + fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData), + ) + } + r.client = client.Client +} + +// Metadata implements resource.Resource +func (r *resourceTFESAMLSettings) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_saml_settings" +} + +// Schema implements resource.Resource +func (r *resourceTFESAMLSettings) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Description: "Whether or not SAML single sign-on is enabled", + Computed: true, + }, + "debug": schema.BoolAttribute{ + Description: "When sign-on fails and this is enabled, the SAMLResponse XML will be displayed on the login page", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "authn_requests_signed": schema.BoolAttribute{ + Description: "Ensure that messages are signed", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "want_assertions_signed": schema.BoolAttribute{ + Description: "Ensure that elements are signed", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "team_management_enabled": schema.BoolAttribute{ + Description: "Set it to false if you would rather use Terraform Enterprise to manage team membership", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "old_idp_cert": schema.StringAttribute{ + Computed: true, + }, + "idp_cert": schema.StringAttribute{ + Description: "Identity Provider Certificate specifies the PEM encoded X.509 Certificate as provided by the IdP configuration", + Required: true, + }, + "slo_endpoint_url": schema.StringAttribute{ + Description: "Single Log Out URL specifies the HTTPS endpoint on your IdP for single logout requests. This value is provided by the IdP configuration", + Required: true, + }, + "sso_endpoint_url": schema.StringAttribute{ + Description: "Single Sign On URL specifies the HTTPS endpoint on your IdP for single sign-on requests. This value is provided by the IdP configuration", + Required: true, + }, + "attr_username": schema.StringAttribute{ + Description: "Username Attribute Name specifies the name of the SAML attribute that determines the user's username", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(samlDefaultAttrUsername), + }, + "attr_site_admin": schema.StringAttribute{ + Description: "Specifies the role for site admin access. Overrides the \"Site Admin Role\" method", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(samlDefaultAttrSiteAdmin), + }, + "attr_groups": schema.StringAttribute{ + Description: "Team Attribute Name specifies the name of the SAML attribute that determines team membership", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(samlDefaultAttrGroups), + }, + "site_admin_role": schema.StringAttribute{ + Description: "Specifies the role for site admin access, provided in the list of roles sent in the Team Attribute Name attribute", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(samlDefaultSiteAdminRole), + }, + "sso_api_token_session_timeout": schema.Int64Attribute{ + Description: "Specifies the Single Sign On session timeout in seconds. Defaults to 14 days", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(samlDefaultSSOAPITokenSessionTimeoutSeconds), + }, + "acs_consumer_url": schema.StringAttribute{ + Description: "ACS Consumer (Recipient) URL", + Computed: true, + }, + "metadata_url": schema.StringAttribute{ + Description: "Metadata (Audience) URL", + Computed: true, + }, + "certificate": schema.StringAttribute{ + Description: "The certificate used for request and assertion signing", + Optional: true, + Computed: true, + }, + "private_key": schema.StringAttribute{ + Description: "The private key used for request and assertion signing", + Default: stringdefault.StaticString(""), + Optional: true, + Computed: true, + Sensitive: true, + }, + "signature_signing_method": schema.StringAttribute{ + Description: fmt.Sprintf("Signature Signing Method. Must be either `%s` or `%s`. Defaults to `%s`", samlSignatureMethodSHA1, samlSignatureMethodSHA256, samlSignatureMethodSHA256), + Optional: true, + Computed: true, + Default: stringdefault.StaticString(samlSignatureMethodSHA256), + Validators: []validator.String{ + stringvalidator.OneOf( + samlSignatureMethodSHA1, + samlSignatureMethodSHA256, + ), + }, + }, + "signature_digest_method": schema.StringAttribute{ + Description: fmt.Sprintf("Signature Digest Method. Must be either `%s` or `%s`. Defaults to `%s`", samlSignatureMethodSHA1, samlSignatureMethodSHA256, samlSignatureMethodSHA256), + Optional: true, + Computed: true, + Default: stringdefault.StaticString(samlSignatureMethodSHA256), + Validators: []validator.String{ + stringvalidator.OneOf( + samlSignatureMethodSHA1, + samlSignatureMethodSHA256, + ), + }, + }, + }, + } +} + +// Read implements resource.Resource +func (r *resourceTFESAMLSettings) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var m modelTFESAMLSettings + diags := req.State.Get(ctx, &m) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + samlSettings, err := r.client.Admin.Settings.SAML.Read(ctx) + if err != nil { + resp.Diagnostics.AddError("Error reading SAML Settings", "Could not read SAML Settings, unexpected error: "+err.Error()) + return + } + + result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey) + diags = resp.State.Set(ctx, &result) + resp.Diagnostics.Append(diags...) +} + +// Create implements resource.Resource +func (r *resourceTFESAMLSettings) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var m modelTFESAMLSettings + diags := req.Plan.Get(ctx, &m) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Create SAML Settings") + samlSettings, err := r.updateSAMLSettings(ctx, m) + if err != nil { + resp.Diagnostics.AddError("Error creating SAML Settings", "Could not set SAML Settings, unexpected error: "+err.Error()) + return + } + + result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey) + diags = resp.State.Set(ctx, &result) + resp.Diagnostics.Append(diags...) +} + +// Update implements resource.Resource +func (r *resourceTFESAMLSettings) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var m modelTFESAMLSettings + diags := req.Plan.Get(ctx, &m) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Update SAML Settings") + samlSettings, err := r.updateSAMLSettings(ctx, m) + if err != nil { + resp.Diagnostics.AddError("Error updating SAML Settings", "Could not set SAML Settings, unexpected error: "+err.Error()) + return + } + + result := modelFromTFEAdminSAMLSettings(*samlSettings, m.PrivateKey) + diags = resp.State.Set(ctx, &result) + resp.Diagnostics.Append(diags...) +} + +// Delete disables the SAML Settings and then removes the resource from the state file. You cannot delete TFE SAML Settings, only disable them +func (r *resourceTFESAMLSettings) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var m modelTFESAMLSettings + diags := req.State.Get(ctx, &m) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Delete SAML Settings") + _, err := r.client.Admin.Settings.SAML.Update(ctx, tfe.AdminSAMLSettingsUpdateOptions{ + Enabled: basetypes.NewBoolValue(false).ValueBoolPointer(), + Debug: basetypes.NewBoolValue(false).ValueBoolPointer(), + AuthnRequestsSigned: basetypes.NewBoolValue(false).ValueBoolPointer(), + WantAssertionsSigned: basetypes.NewBoolValue(false).ValueBoolPointer(), + TeamManagementEnabled: basetypes.NewBoolValue(false).ValueBoolPointer(), + IDPCert: basetypes.NewStringValue("").ValueStringPointer(), + SLOEndpointURL: basetypes.NewStringValue("").ValueStringPointer(), + SSOEndpointURL: basetypes.NewStringValue("").ValueStringPointer(), + AttrUsername: basetypes.NewStringValue(samlDefaultAttrUsername).ValueStringPointer(), + AttrSiteAdmin: basetypes.NewStringValue(samlDefaultAttrSiteAdmin).ValueStringPointer(), + AttrGroups: basetypes.NewStringValue(samlDefaultAttrGroups).ValueStringPointer(), + SiteAdminRole: basetypes.NewStringValue(samlDefaultSiteAdminRole).ValueStringPointer(), + SSOAPITokenSessionTimeout: tfe.Int(int(samlDefaultSSOAPITokenSessionTimeoutSeconds)), + Certificate: basetypes.NewStringValue("").ValueStringPointer(), + PrivateKey: basetypes.NewStringValue("").ValueStringPointer(), + SignatureSigningMethod: basetypes.NewStringValue(samlSignatureMethodSHA256).ValueStringPointer(), + SignatureDigestMethod: basetypes.NewStringValue(samlSignatureMethodSHA256).ValueStringPointer(), + }) + if err != nil { + resp.Diagnostics.AddError("Error deleting SAML Settings", "Could not disable SAML Settings, unexpected error: "+err.Error()) + return + } +} + +// ImportState implements resource.ResourceWithImportState +func (r *resourceTFESAMLSettings) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + samlSettings, err := r.client.Admin.Settings.SAML.Read(ctx) + if err != nil { + resp.Diagnostics.AddError("Error importing SAML Settings", "Could not retrieve SAML Settings, unexpected error: "+err.Error()) + return + } + + result := modelFromTFEAdminSAMLSettings(*samlSettings, types.StringValue("")) + diags := resp.State.Set(ctx, &result) + resp.Diagnostics.Append(diags...) +} + +var ( + _ resource.Resource = &resourceTFESAMLSettings{} + _ resource.ResourceWithConfigure = &resourceTFESAMLSettings{} + _ resource.ResourceWithImportState = &resourceTFESAMLSettings{} +) + +// NewSAMLSettingsResource is a resource function for the framework provider. +func NewSAMLSettingsResource() resource.Resource { + return &resourceTFESAMLSettings{} +} + +// updateSAMLSettings was created to keep the code DRY. It is used in both Create and Update functions +func (r *resourceTFESAMLSettings) updateSAMLSettings(ctx context.Context, m modelTFESAMLSettings) (*tfe.AdminSAMLSetting, error) { + s, err := r.client.Admin.Settings.SAML.Update(ctx, tfe.AdminSAMLSettingsUpdateOptions{ + Enabled: basetypes.NewBoolValue(true).ValueBoolPointer(), + Debug: m.Debug.ValueBoolPointer(), + IDPCert: m.IDPCert.ValueStringPointer(), + Certificate: m.Certificate.ValueStringPointer(), + PrivateKey: m.PrivateKey.ValueStringPointer(), + SLOEndpointURL: m.SLOEndpointURL.ValueStringPointer(), + SSOEndpointURL: m.SSOEndpointURL.ValueStringPointer(), + AttrUsername: m.AttrUsername.ValueStringPointer(), + AttrGroups: m.AttrGroups.ValueStringPointer(), + AttrSiteAdmin: m.AttrSiteAdmin.ValueStringPointer(), + SiteAdminRole: m.SiteAdminRole.ValueStringPointer(), + SSOAPITokenSessionTimeout: tfe.Int(int(m.SSOAPITokenSessionTimeout.ValueInt64())), + TeamManagementEnabled: m.TeamManagementEnabled.ValueBoolPointer(), + AuthnRequestsSigned: m.AuthnRequestsSigned.ValueBoolPointer(), + WantAssertionsSigned: m.WantAssertionsSigned.ValueBoolPointer(), + SignatureSigningMethod: m.SignatureSigningMethod.ValueStringPointer(), + SignatureDigestMethod: m.SignatureDigestMethod.ValueStringPointer(), + }) + if err != nil { + return s, fmt.Errorf("failed to update SAML Settings %v", err) + } + return s, err +} diff --git a/tfe/resource_tfe_saml_settings_test.go b/tfe/resource_tfe_saml_settings_test.go new file mode 100644 index 000000000..9a08206e5 --- /dev/null +++ b/tfe/resource_tfe_saml_settings_test.go @@ -0,0 +1,315 @@ +package tfe + +import ( + "fmt" + "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "strconv" + "testing" +) + +const testResourceName = "tfe_saml_settings.foobar" + +func TestAccTFESAMLSettings_basic(t *testing.T) { + s := tfe.AdminSAMLSetting{ + IDPCert: "testIDPCertBasic", + SLOEndpointURL: "https://foobar.com/slo_endpoint_url", + SSOEndpointURL: "https://foobar.com/sso_endpoint_url", + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccTFESAMLSettingsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFESAMLSettings_basic(s), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testResourceName, "enabled", "true"), + resource.TestCheckResourceAttr(testResourceName, "debug", "false"), + resource.TestCheckResourceAttr(testResourceName, "authn_requests_signed", "false"), + resource.TestCheckResourceAttr(testResourceName, "want_assertions_signed", "false"), + resource.TestCheckResourceAttr(testResourceName, "team_management_enabled", "false"), + resource.TestCheckResourceAttr(testResourceName, "idp_cert", s.IDPCert), + resource.TestCheckResourceAttr(testResourceName, "slo_endpoint_url", s.SLOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "sso_endpoint_url", s.SSOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "attr_username", samlDefaultAttrUsername), + resource.TestCheckResourceAttr(testResourceName, "attr_site_admin", samlDefaultAttrSiteAdmin), + resource.TestCheckResourceAttr(testResourceName, "attr_groups", samlDefaultAttrGroups), + resource.TestCheckResourceAttr(testResourceName, "site_admin_role", samlDefaultSiteAdminRole), + resource.TestCheckResourceAttr(testResourceName, "sso_api_token_session_timeout", strconv.Itoa(int(samlDefaultSSOAPITokenSessionTimeoutSeconds))), + resource.TestCheckResourceAttrSet(testResourceName, "acs_consumer_url"), + resource.TestCheckResourceAttrSet(testResourceName, "metadata_url"), + resource.TestCheckResourceAttr(testResourceName, "signature_signing_method", samlSignatureMethodSHA256), + resource.TestCheckResourceAttr(testResourceName, "signature_digest_method", samlSignatureMethodSHA256), + ), + }, + }, + }) +} + +func TestAccTFESAMLSettings_full(t *testing.T) { + s := tfe.AdminSAMLSetting{ + IDPCert: "testIDPCertFull", + SLOEndpointURL: "https://foobar.com/slo_endpoint_url", + SSOEndpointURL: "https://foobar.com/sso_endpoint_url", + Debug: true, + AuthnRequestsSigned: true, + WantAssertionsSigned: true, + TeamManagementEnabled: false, + AttrUsername: "Foo" + samlDefaultAttrUsername, + AttrSiteAdmin: "Foo" + samlDefaultAttrSiteAdmin, + AttrGroups: "Foo" + samlDefaultAttrGroups, + SiteAdminRole: "foo-" + samlDefaultSiteAdminRole, + SSOAPITokenSessionTimeout: 1101100, + Certificate: "TestCertificateFull", + PrivateKey: "TestPrivateKeyFull", + SignatureSigningMethod: samlSignatureMethodSHA1, + SignatureDigestMethod: samlSignatureMethodSHA256, + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccTFESAMLSettingsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFESAMLSettings_full(s), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testResourceName, "enabled", "true"), + resource.TestCheckResourceAttr(testResourceName, "debug", strconv.FormatBool(s.Debug)), + resource.TestCheckResourceAttr(testResourceName, "authn_requests_signed", strconv.FormatBool(s.AuthnRequestsSigned)), + resource.TestCheckResourceAttr(testResourceName, "want_assertions_signed", strconv.FormatBool(s.WantAssertionsSigned)), + resource.TestCheckResourceAttr(testResourceName, "team_management_enabled", strconv.FormatBool(s.TeamManagementEnabled)), + resource.TestCheckResourceAttr(testResourceName, "idp_cert", s.IDPCert), + resource.TestCheckResourceAttr(testResourceName, "slo_endpoint_url", s.SLOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "sso_endpoint_url", s.SSOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "attr_username", s.AttrUsername), + resource.TestCheckResourceAttr(testResourceName, "attr_site_admin", s.AttrSiteAdmin), + resource.TestCheckResourceAttr(testResourceName, "attr_groups", s.AttrGroups), + resource.TestCheckResourceAttr(testResourceName, "site_admin_role", s.SiteAdminRole), + resource.TestCheckResourceAttr(testResourceName, "sso_api_token_session_timeout", strconv.Itoa(s.SSOAPITokenSessionTimeout)), + resource.TestCheckResourceAttrSet(testResourceName, "acs_consumer_url"), + resource.TestCheckResourceAttrSet(testResourceName, "metadata_url"), + resource.TestCheckResourceAttr(testResourceName, "signature_signing_method", s.SignatureSigningMethod), + resource.TestCheckResourceAttr(testResourceName, "signature_digest_method", s.SignatureDigestMethod), + ), + }, + }, + }) +} + +func TestAccTFESAMLSettings_update(t *testing.T) { + s := tfe.AdminSAMLSetting{ + IDPCert: "testIDPCertUpdateInit", + SLOEndpointURL: "https://foobar.com/slo_endpoint_url", + SSOEndpointURL: "https://foobar.com/sso_endpoint_url", + } + updatedSetting := tfe.AdminSAMLSetting{ + IDPCert: "testIDPCertUpdateInit", + SLOEndpointURL: "https://foobar-updated.com/slo_endpoint_url", + SSOEndpointURL: "https://foobar-updated.com/sso_endpoint_url", + Debug: true, + AuthnRequestsSigned: true, + WantAssertionsSigned: true, + TeamManagementEnabled: false, + AttrUsername: "FooUpdate" + samlDefaultAttrUsername, + AttrSiteAdmin: "FooUpdate" + samlDefaultAttrSiteAdmin, + AttrGroups: "FooUpdate" + samlDefaultAttrGroups, + SiteAdminRole: "foo-update-" + samlDefaultSiteAdminRole, + SSOAPITokenSessionTimeout: 1234567, + Certificate: "TestCertificateUpdate", + PrivateKey: "TestPrivateKeyUpdate", + SignatureSigningMethod: samlSignatureMethodSHA1, + SignatureDigestMethod: samlSignatureMethodSHA256, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccTFESAMLSettingsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFESAMLSettings_basic(s), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testResourceName, "enabled", "true"), + resource.TestCheckResourceAttr(testResourceName, "debug", "false"), + resource.TestCheckResourceAttr(testResourceName, "authn_requests_signed", "false"), + resource.TestCheckResourceAttr(testResourceName, "want_assertions_signed", "false"), + resource.TestCheckResourceAttr(testResourceName, "team_management_enabled", "false"), + resource.TestCheckResourceAttr(testResourceName, "idp_cert", s.IDPCert), + resource.TestCheckResourceAttr(testResourceName, "slo_endpoint_url", s.SLOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "sso_endpoint_url", s.SSOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "attr_username", samlDefaultAttrUsername), + resource.TestCheckResourceAttr(testResourceName, "attr_site_admin", samlDefaultAttrSiteAdmin), + resource.TestCheckResourceAttr(testResourceName, "attr_groups", samlDefaultAttrGroups), + resource.TestCheckResourceAttr(testResourceName, "site_admin_role", samlDefaultSiteAdminRole), + resource.TestCheckResourceAttr(testResourceName, "sso_api_token_session_timeout", strconv.Itoa(int(samlDefaultSSOAPITokenSessionTimeoutSeconds))), + resource.TestCheckResourceAttrSet(testResourceName, "acs_consumer_url"), + resource.TestCheckResourceAttrSet(testResourceName, "metadata_url"), + resource.TestCheckResourceAttr(testResourceName, "signature_signing_method", samlSignatureMethodSHA256), + resource.TestCheckResourceAttr(testResourceName, "signature_digest_method", samlSignatureMethodSHA256), + ), + }, + { + Config: testAccTFESAMLSettings_full(updatedSetting), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testResourceName, "enabled", "true"), + resource.TestCheckResourceAttr(testResourceName, "debug", strconv.FormatBool(updatedSetting.Debug)), + resource.TestCheckResourceAttr(testResourceName, "authn_requests_signed", strconv.FormatBool(updatedSetting.AuthnRequestsSigned)), + resource.TestCheckResourceAttr(testResourceName, "want_assertions_signed", strconv.FormatBool(updatedSetting.WantAssertionsSigned)), + resource.TestCheckResourceAttr(testResourceName, "team_management_enabled", strconv.FormatBool(updatedSetting.TeamManagementEnabled)), + resource.TestCheckResourceAttr(testResourceName, "idp_cert", updatedSetting.IDPCert), + resource.TestCheckResourceAttr(testResourceName, "slo_endpoint_url", updatedSetting.SLOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "sso_endpoint_url", updatedSetting.SSOEndpointURL), + resource.TestCheckResourceAttr(testResourceName, "attr_username", updatedSetting.AttrUsername), + resource.TestCheckResourceAttr(testResourceName, "attr_site_admin", updatedSetting.AttrSiteAdmin), + resource.TestCheckResourceAttr(testResourceName, "attr_groups", updatedSetting.AttrGroups), + resource.TestCheckResourceAttr(testResourceName, "site_admin_role", updatedSetting.SiteAdminRole), + resource.TestCheckResourceAttr(testResourceName, "sso_api_token_session_timeout", strconv.Itoa(updatedSetting.SSOAPITokenSessionTimeout)), + resource.TestCheckResourceAttrSet(testResourceName, "acs_consumer_url"), + resource.TestCheckResourceAttrSet(testResourceName, "metadata_url"), + resource.TestCheckResourceAttr(testResourceName, "signature_signing_method", updatedSetting.SignatureSigningMethod), + resource.TestCheckResourceAttr(testResourceName, "signature_digest_method", updatedSetting.SignatureDigestMethod), + ), + }, + }, + }) +} + +func TestAccTFESAMLSettings_import(t *testing.T) { + idpCert := "testIDPCertImport" + slo := "https://foobar-import.com/slo_endpoint_url" + sso := "https://foobar-import.com/sso_endpoint_url" + s := tfe.AdminSAMLSetting{ + IDPCert: idpCert, + SLOEndpointURL: slo, + SSOEndpointURL: sso, + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccTFESAMLSettingsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFESAMLSettings_basic(s), + }, + { + ResourceName: testResourceName, + ImportState: true, + ImportStateCheck: func(s []*terraform.InstanceState) error { + if len(s) != 1 { + return fmt.Errorf("expected 1 state: %+v", s) + } + rs := s[0] + if rs.Attributes["private_key"] != "" { + return fmt.Errorf("expected private_key attribute to not be set, received: %s", rs.Attributes["private_key"]) + + } + if rs.Attributes["idp_cert"] != idpCert { + return fmt.Errorf("expected idp_cert attribute to be equal to %s, received: %s", idpCert, rs.Attributes["idp_cert"]) + + } + if rs.Attributes["slo_endpoint_url"] != slo { + return fmt.Errorf("expected slo_endpoint_url attribute to be equal to %s, received: %s", slo, rs.Attributes["slo_endpoint_url"]) + + } + if rs.Attributes["sso_endpoint_url"] != sso { + return fmt.Errorf("expected sso_endpoint_url attribute to be equal to %s, received: %s", sso, rs.Attributes["sso_endpoint_url"]) + + } + return nil + }, + }, + }, + }) +} + +func testAccTFESAMLSettingsDestroy(_ *terraform.State) error { + s, err := testAccProvider.Meta().(ConfiguredClient).Client.Admin.Settings.SAML.Read(ctx) + if err != nil { + return fmt.Errorf("failed to read SAML Settings: %v", err) + } + if s.Enabled { + return fmt.Errorf("SAML settings are still enabled") + } + if s.Debug { + return fmt.Errorf("SAML settings debug is set to true") + } + if s.AuthnRequestsSigned { + return fmt.Errorf("SAML settings AuthnRequestsSigned is set to true") + } + if s.WantAssertionsSigned { + return fmt.Errorf("SAML settings WantAssertionsSigned is set to true") + } + if s.TeamManagementEnabled { + return fmt.Errorf("SAML settings TeamManagementEnabled is set to true") + } + if s.IDPCert != "" { + return fmt.Errorf("SAML settings IDPCert is not empty: `%s`", s.IDPCert) + } + if s.SLOEndpointURL != "" { + return fmt.Errorf("SAML settings SLOEndpointURL is not empty: `%s`", s.SLOEndpointURL) + } + if s.SSOEndpointURL != "" { + return fmt.Errorf("SAML settings SSOEndpointURL is not empty: `%s`", s.SSOEndpointURL) + } + if s.Certificate != "" { + return fmt.Errorf("SAML settings Certificate is not empty: `%s`", s.Certificate) + } + if s.PrivateKey != "" { + return fmt.Errorf("SAML settings PrivateKey is not empty") + } + if s.AttrUsername != samlDefaultAttrUsername { + return fmt.Errorf("SAML settings AttrUsername is not `%s`", samlDefaultAttrUsername) + } + if s.AttrSiteAdmin != samlDefaultAttrSiteAdmin { + return fmt.Errorf("SAML settings AttrSiteAdmin is not `%s`", samlDefaultAttrSiteAdmin) + } + if s.AttrGroups != samlDefaultAttrGroups { + return fmt.Errorf("SAML settings AttrGroups is not `%s`", samlDefaultAttrGroups) + } + if s.SiteAdminRole != samlDefaultSiteAdminRole { + return fmt.Errorf("SAML settings SiteAdminRole is not `%s`", samlDefaultSiteAdminRole) + } + if s.SignatureSigningMethod != samlSignatureMethodSHA256 { + return fmt.Errorf("SAML settings SignatureSigningMethod is not `%s`", samlSignatureMethodSHA256) + } + if s.SignatureDigestMethod != samlSignatureMethodSHA256 { + return fmt.Errorf("SAML settings SignatureDigestMethod is not `%s`", samlSignatureMethodSHA256) + } + if s.SSOAPITokenSessionTimeout != int(samlDefaultSSOAPITokenSessionTimeoutSeconds) { + return fmt.Errorf("SAML settings SignatureDigestMethod is not `%d`", samlDefaultSSOAPITokenSessionTimeoutSeconds) + } + return nil +} + +func testAccTFESAMLSettings_basic(s tfe.AdminSAMLSetting) string { + return fmt.Sprintf(` +resource "tfe_saml_settings" "foobar" { + idp_cert = "%s" + slo_endpoint_url = "%s" + sso_endpoint_url = "%s" +}`, s.IDPCert, s.SLOEndpointURL, s.SSOEndpointURL) +} + +func testAccTFESAMLSettings_full(s tfe.AdminSAMLSetting) string { + return fmt.Sprintf(` +resource "tfe_saml_settings" "foobar" { + idp_cert = "%s" + slo_endpoint_url = "%s" + sso_endpoint_url = "%s" + debug = %t + authn_requests_signed = %t + want_assertions_signed = %t + team_management_enabled = %t + attr_username = "%s" + attr_site_admin = "%s" + attr_groups = "%s" + site_admin_role = "%s" + sso_api_token_session_timeout = %d + certificate = "%s" + private_key = "%s" + signature_signing_method = "%s" + signature_digest_method = "%s" +}`, s.IDPCert, s.SLOEndpointURL, s.SSOEndpointURL, s.Debug, s.AuthnRequestsSigned, s.WantAssertionsSigned, s.TeamManagementEnabled, s.AttrUsername, s.AttrSiteAdmin, s.AttrGroups, s.SiteAdminRole, s.SSOAPITokenSessionTimeout, s.Certificate, s.PrivateKey, s.SignatureSigningMethod, s.SignatureDigestMethod) +} diff --git a/tfe/tool_helpers.go b/tfe/tool_helpers.go index fde0958bb..791395b56 100644 --- a/tfe/tool_helpers.go +++ b/tfe/tool_helpers.go @@ -5,7 +5,6 @@ package tfe import ( "fmt" - tfe "github.com/hashicorp/go-tfe" ) diff --git a/website/docs/d/saml_settings.html.markdown b/website/docs/d/saml_settings.html.markdown index 9844165b6..2e6b83936 100644 --- a/website/docs/d/saml_settings.html.markdown +++ b/website/docs/d/saml_settings.html.markdown @@ -57,3 +57,7 @@ The following attributes are exported: * `acs_consumer_url` - ACS Consumer (Recipient) URL. * `metadata_url` - Metadata (Audience) URL. * `certificate` - Request and assertion signing certificate. +* `certificate` - Request and assertion signing certificate. +* `private_key` - The private key used for request and assertion signing. +* `signature_signing_method` - Signature Signing Method. +* `signature_digest_method` - Signature Digest Method. diff --git a/website/docs/r/saml_settings.html.markdown b/website/docs/r/saml_settings.html.markdown new file mode 100644 index 000000000..40c0764ce --- /dev/null +++ b/website/docs/r/saml_settings.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "tfe" +page_title: "Terraform Enterprise: tfe_saml_settings" +description: |- + Manages SAML Settings. +--- + +# tfe_saml_settings + +(TFE only) Creates, updates and destroys SAML Settings. + +## Example Usage + +Basic usage for SAML Settings: + +```hcl +resource "tfe_saml_settings" "this" { + idp_cert = "foobarCertificate" + slo_endpoint_url = "https://example.com/slo_endpoint_url" + sso_endpoint_url = "https://example.com/sso_endpoint_url" + } +``` + +## Argument Reference + +The following arguments are supported: + +* `idp_cert` - (Required) Identity Provider Certificate specifies the PEM encoded X.509 Certificate as provided by the IdP configuration. +* `slo_endpoint_url` - (Required) Single Log Out URL specifies the HTTPS endpoint on your IdP for single logout requests. This value is provided by the IdP configuration. +* `sso_endpoint_url` - (Required) Single Sign On URL specifies the HTTPS endpoint on your IdP for single sign-on requests. This value is provided by the IdP configuration. +* `debug` - (Optional) When sign-on fails and this is enabled, the SAMLResponse XML will be displayed on the login page. +* `authn_requests_signed` - (Optional) Whether to ensure that `` messages are signed. +* `want_assertions_signed` - (Optional) Whether to ensure that `` elements are signed. +* `team_management_enabled` - (Optional) Set it to false if you would rather use Terraform Enterprise to manage team membership. +* `attr_username` - (Optional) Username Attribute Name specifies the name of the SAML attribute that determines the user's username. +* `attr_site_admin` - (Optional) Specifies the role for site admin access. Overrides the `Site Admin Role` method. +* `attr_groups` - (Optional) Team Attribute Name specifies the name of the SAML attribute that determines team membership. +* `site_admin_role` - (Optional) Specifies the role for site admin access, provided in the list of roles sent in the Team Attribute Name attribute. +* `sso_api_token_session_timeout` - (Optional) Specifies the Single Sign On session timeout in seconds. Defaults to 14 days. +* `certificate` - (Optional) The certificate used for request and assertion signing. +* `private_key` - (Optional) The private key used for request and assertion signing. +* `signature_signing_method` - (Optional) Signature Signing Method. Must be either `SHA1` or `SHA256`. Defaults to `SHA256`. +* `signature_digest_method` - (Optional) Signature Digest Method. Must be either `SHA1` or `SHA256`. Defaults to `SHA256`. + +## Attributes Reference + +* `id` - The ID of the SAML Settings. Always `saml`. +* `acs_consumer_url` - ACS Consumer (Recipient) URL. +* `metadata_url` - Metadata (Audience) URL. +* `old_idp_cert` - Value of the old IDP Certificate. + +## Import + +SAML Settings can be imported. + +```shell +terraform import tfe_saml_settings.this saml +```