diff --git a/.changelog/0.14.0.toml b/.changelog/0.14.0.toml
index fa2e8545..f2afb864 100644
--- a/.changelog/0.14.0.toml
+++ b/.changelog/0.14.0.toml
@@ -3,6 +3,8 @@ title = ""
description = ""
[[features]]
+title = "New resource"
+description = "`oxide_silo_saml_identity_provider` [#442](https://github.com/oxidecomputer/terraform-provider-oxide/pull/442)."
[[enhancements]]
title = ""
diff --git a/docs/resources/oxide_silo_saml_identity_provider.md b/docs/resources/oxide_silo_saml_identity_provider.md
new file mode 100644
index 00000000..e88ab6e0
--- /dev/null
+++ b/docs/resources/oxide_silo_saml_identity_provider.md
@@ -0,0 +1,115 @@
+---
+page_title: "oxide_silo_saml_identity_provider Resource - terraform-provider-oxide"
+---
+
+# oxide_silo_saml_identity_provider (Resource)
+
+Manages a SAML identity provider (IdP) for an Oxide silo.
+
+-> This resource does not support updates. All attributes are immutable once
+created.
+
+-> This resource does not support deletion from the Oxide API. When destroyed in
+Terraform, it will be removed from state but will continue to exist in Oxide.
+
+## Example Usage
+
+### With URL Metadata Source
+
+```hcl
+resource "oxide_silo_saml_identity_provider" "example" {
+ silo = oxide_silo.example.id
+ name = "keycloak"
+ description = "Managed by Terraform."
+ group_attribute_name = "groups"
+ idp_entity_id = "https://keycloak.example.com/realms/oxide"
+ acs_url = "https://example.com/saml/acs"
+ slo_url = "https://example.com/saml/logout"
+ sp_client_id = "oxide-sp"
+ technical_contact_email = "admin@example.com"
+
+ idp_metadata_source = {
+ type = "url"
+ url = "https://keycloak.example.com/realms/oxide/protocol/saml/descriptor"
+ }
+}
+```
+
+### With Base64-Encoded XML Metadata
+
+```hcl
+resource "oxide_silo_saml_identity_provider" "example" {
+ silo = oxide_silo.example.id
+ name = "custom-idp"
+ description = "Custom SAML identity provider"
+ idp_entity_id = "https://idp.example.com"
+ acs_url = "https://example.com/saml/acs"
+ slo_url = "https://example.com/saml/logout"
+ sp_client_id = "oxide-sp"
+ technical_contact_email = "admin@example.com"
+
+ idp_metadata_source = {
+ type = "base64_encoded_xml"
+ data = base64encode(file("${path.module}/idp-metadata.xml"))
+ }
+
+ signing_keypair = {
+ private_key = base64encode(file("${path.module}/saml-key.pem"))
+ public_cert = base64encode(file("${path.module}/saml-cert.pem"))
+ }
+}
+```
+
+## Schema
+
+### Required
+
+- `acs_url` (String) URL where the identity provider should send the SAML response.
+- `description` (String) Free-form text describing the SAML identity provider.
+- `idp_entity_id` (String) Identity provider's entity ID.
+- `idp_metadata_source` (Attributes) Source of identity provider metadata (URL or base64-encoded XML). (see [below for nested schema](#nestedatt--idp_metadata_source))
+- `name` (String) Unique, immutable, user-controlled identifier of the SAML identity provider. Maximum length is 63 characters.
+- `silo` (String) Name or ID of the silo.
+- `slo_url` (String) URL where the identity provider should send logout requests.
+- `sp_client_id` (String) Service provider's client ID.
+- `technical_contact_email` (String) Technical contact email for SAML configuration.
+
+### Optional
+
+- `group_attribute_name` (String) SAML attribute that holds a user's group membership.
+- `signing_keypair` (Attributes) RSA private key and public certificate for signing SAML requests. (see [below for nested schema](#nestedatt--signing_keypair))
+- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
+
+### Read-Only
+
+- `id` (String) Unique, immutable, system-controlled identifier of the SAML identity provider.
+- `time_created` (String) Timestamp of when this SAML identity provider was created.
+- `time_modified` (String) Timestamp of when this SAML identity provider was last modified.
+
+
+### Nested Schema for `idp_metadata_source`
+
+Required:
+
+- `type` (String) The type of metadata source. Must be one of: `url`, `base64_encoded_xml`.
+
+Optional:
+
+- `data` (String) Base64-encoded XML metadata (required when type is `base64_encoded_xml`). Conflicts with `url`.
+- `url` (String) URL to fetch metadata from (required when type is `url`). Conflicts with `data`.
+
+
+### Nested Schema for `signing_keypair`
+
+Required:
+
+- `private_key` (String, Sensitive) RSA private key (base64 encoded).
+- `public_cert` (String) Public certificate (base64 encoded).
+
+
+### Nested Schema for `timeouts`
+
+Optional:
+
+- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
+- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
diff --git a/docs/resources/oxide_vpc_firewall_rules.md b/docs/resources/oxide_vpc_firewall_rules.md
index 09a21170..4636c169 100644
--- a/docs/resources/oxide_vpc_firewall_rules.md
+++ b/docs/resources/oxide_vpc_firewall_rules.md
@@ -70,12 +70,12 @@ resource "oxide_vpc_firewall_rules" "example" {
},
# Echo Reply types.
{
- type = "icmp",
+ type = "icmp",
icmp_type = 0
},
# Echo Reply types with codes 1-3.
{
- type = "icmp",
+ type = "icmp",
icmp_type = 0
icmp_code = "1-3"
},
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 62f1f228..7cf1358f 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -199,5 +199,6 @@ func (p *oxideProvider) Resources(_ context.Context) []func() resource.Resource
NewVPCSubnetResource,
NewFloatingIPResource,
NewSiloResource,
+ NewSiloSamlIdentityProviderResource,
}
}
diff --git a/internal/provider/resource_silo_saml_identity_provider.go b/internal/provider/resource_silo_saml_identity_provider.go
new file mode 100644
index 00000000..f4cdc619
--- /dev/null
+++ b/internal/provider/resource_silo_saml_identity_provider.go
@@ -0,0 +1,331 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "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/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "github.com/oxidecomputer/oxide.go/oxide"
+)
+
+var (
+ _ resource.Resource = (*siloSamlIdentityProvider)(nil)
+ _ resource.ResourceWithConfigure = (*siloSamlIdentityProvider)(nil)
+)
+
+func NewSiloSamlIdentityProviderResource() resource.Resource {
+ return &siloSamlIdentityProvider{}
+}
+
+type siloSamlIdentityProvider struct {
+ client *oxide.Client
+}
+
+type siloSamlIdentityProviderResourceModel struct {
+ ID types.String `tfsdk:"id"`
+ Name types.String `tfsdk:"name"`
+ Description types.String `tfsdk:"description"`
+ Silo types.String `tfsdk:"silo"`
+ AcsUrl types.String `tfsdk:"acs_url"`
+ IdpEntityId types.String `tfsdk:"idp_entity_id"`
+ SloUrl types.String `tfsdk:"slo_url"`
+ SpClientId types.String `tfsdk:"sp_client_id"`
+ TechnicalContactEmail types.String `tfsdk:"technical_contact_email"`
+ IdpMetadataSource *siloSamlIdentityProviderMetadataSourceModel `tfsdk:"idp_metadata_source"`
+ GroupAttributeName types.String `tfsdk:"group_attribute_name"`
+ SigningKeypair *siloSamlIdentityProviderSigningKeypairModel `tfsdk:"signing_keypair"`
+ TimeCreated types.String `tfsdk:"time_created"`
+ TimeModified types.String `tfsdk:"time_modified"`
+ Timeouts timeouts.Value `tfsdk:"timeouts"`
+}
+
+type siloSamlIdentityProviderSigningKeypairModel struct {
+ PrivateKey types.String `tfsdk:"private_key"`
+ PublicCert types.String `tfsdk:"public_cert"`
+}
+
+type siloSamlIdentityProviderMetadataSourceModel struct {
+ Type types.String `tfsdk:"type"`
+ Url types.String `tfsdk:"url"`
+ Data types.String `tfsdk:"data"`
+}
+
+func (r *siloSamlIdentityProvider) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = "oxide_silo_saml_identity_provider"
+}
+
+func (r *siloSamlIdentityProvider) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ r.client = req.ProviderData.(*oxide.Client)
+}
+
+func (r *siloSamlIdentityProvider) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "Manages a SAML identity provider (IdP) for an Oxide silo.",
+ Attributes: map[string]schema.Attribute{
+ "silo": schema.StringAttribute{
+ Required: true,
+ Description: "Name or ID of the silo.",
+ },
+ "acs_url": schema.StringAttribute{
+ Required: true,
+ Description: "URL where the identity provider should send the SAML response.",
+ },
+ "description": schema.StringAttribute{
+ Required: true,
+ Description: "Free-form text describing the SAML identity provider.",
+ },
+ "group_attribute_name": schema.StringAttribute{
+ Optional: true,
+ Description: "SAML attribute that holds a user's group membership.",
+ },
+ "id": schema.StringAttribute{
+ Computed: true,
+ Description: "Unique, immutable, system-controlled identifier of the SAML identity provider.",
+ },
+ "idp_entity_id": schema.StringAttribute{
+ Required: true,
+ Description: "Identity provider's entity ID.",
+ },
+ "idp_metadata_source": schema.SingleNestedAttribute{
+ Required: true,
+ Description: "Source of identity provider metadata (URL or base64-encoded XML).",
+ Attributes: map[string]schema.Attribute{
+ "type": schema.StringAttribute{
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf(
+ "url",
+ "base64_encoded_xml",
+ ),
+ },
+ },
+ "url": schema.StringAttribute{
+ Optional: true,
+ Description: "URL to fetch metadata from (required when type is `url`).",
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("data")),
+ },
+ },
+ "data": schema.StringAttribute{
+ Optional: true,
+ Description: "Base64-encoded XML metadata (required when type is `base64_encoded_xml`).",
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("url")),
+ },
+ },
+ },
+ },
+ "name": schema.StringAttribute{
+ Required: true,
+ Description: "Unique, immutable, user-controlled identifier of the SAML identity provider.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtMost(63),
+ },
+ },
+ "signing_keypair": schema.SingleNestedAttribute{
+ Optional: true,
+ Description: "RSA private key and public certificate for signing SAML requests.",
+ Attributes: map[string]schema.Attribute{
+ "private_key": schema.StringAttribute{
+ Required: true,
+ Sensitive: true,
+ Description: "RSA private key (base64 encoded).",
+ },
+ "public_cert": schema.StringAttribute{
+ Required: true,
+ Description: "Public certificate (base64 encoded).",
+ },
+ },
+ },
+ "slo_url": schema.StringAttribute{
+ Required: true,
+ Description: "URL where the identity provider should send logout requests.",
+ },
+ "sp_client_id": schema.StringAttribute{
+ Required: true,
+ Description: "Service provider's client ID.",
+ },
+ "technical_contact_email": schema.StringAttribute{
+ Required: true,
+ Description: "Technical contact email for SAML configuration.",
+ },
+ "time_created": schema.StringAttribute{
+ Computed: true,
+ Description: "Timestamp of when this SAML identity provider was created.",
+ },
+ "time_modified": schema.StringAttribute{
+ Computed: true,
+ Description: "Timestamp of when this SAML identity provider was last modified.",
+ },
+ "timeouts": timeouts.Attributes(ctx, timeouts.Opts{
+ Create: true,
+ Read: true,
+ }),
+ },
+ }
+}
+
+func (r *siloSamlIdentityProvider) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan siloSamlIdentityProviderResourceModel
+
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ createTimeout, diags := plan.Timeouts.Create(ctx, defaultTimeout())
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, createTimeout)
+ defer cancel()
+
+ idpMetadataSource := oxide.IdpMetadataSource{
+ Type: oxide.IdpMetadataSourceType(plan.IdpMetadataSource.Type.ValueString()),
+ }
+
+ switch idpMetadataSource.Type {
+ case oxide.IdpMetadataSourceTypeBase64EncodedXml:
+ idpMetadataSource.Data = plan.IdpMetadataSource.Data.ValueString()
+ case oxide.IdpMetadataSourceTypeUrl:
+ idpMetadataSource.Url = plan.IdpMetadataSource.Url.ValueString()
+ }
+
+ params := oxide.SamlIdentityProviderCreateParams{
+ Silo: oxide.NameOrId(plan.Silo.ValueString()),
+ Body: &oxide.SamlIdentityProviderCreate{
+ AcsUrl: plan.AcsUrl.ValueString(),
+ IdpEntityId: plan.IdpEntityId.ValueString(),
+ Name: oxide.Name(plan.Name.ValueString()),
+ SloUrl: plan.SloUrl.ValueString(),
+ SpClientId: plan.SpClientId.ValueString(),
+ TechnicalContactEmail: plan.TechnicalContactEmail.ValueString(),
+ IdpMetadataSource: idpMetadataSource,
+ Description: plan.Description.ValueString(),
+ GroupAttributeName: plan.GroupAttributeName.ValueString(),
+ },
+ }
+
+ if plan.SigningKeypair != nil {
+ params.Body.SigningKeypair = oxide.DerEncodedKeyPair{
+ PrivateKey: plan.SigningKeypair.PrivateKey.ValueString(),
+ PublicCert: plan.SigningKeypair.PublicCert.ValueString(),
+ }
+ }
+
+ idpConfig, err := r.client.SamlIdentityProviderCreate(ctx, params)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error creating SAML identity provider",
+ "API error: "+err.Error(),
+ )
+ return
+ }
+
+ tflog.Trace(ctx, fmt.Sprintf("created SAML identity provider with ID: %v", idpConfig.Id), map[string]any{"success": true})
+
+ plan.ID = types.StringValue(idpConfig.Id)
+ plan.TimeCreated = types.StringValue(idpConfig.TimeCreated.String())
+ plan.TimeModified = types.StringValue(idpConfig.TimeModified.String())
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+func (r *siloSamlIdentityProvider) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state siloSamlIdentityProviderResourceModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ readTimeout, diags := state.Timeouts.Read(ctx, defaultTimeout())
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, readTimeout)
+ defer cancel()
+
+ params := oxide.SamlIdentityProviderViewParams{
+ Provider: oxide.NameOrId(state.ID.ValueString()),
+ }
+
+ idpConfig, err := r.client.SamlIdentityProviderView(ctx, params)
+ if err != nil {
+ if is404(err) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError(
+ "Unable to read SAML identity provider:",
+ "API error: "+err.Error(),
+ )
+ return
+ }
+
+ tflog.Trace(ctx, fmt.Sprintf("read SAML identity provider with ID: %v", idpConfig.Id), map[string]any{"success": true})
+
+ state.ID = types.StringValue(idpConfig.Id)
+ state.TimeCreated = types.StringValue(idpConfig.TimeCreated.String())
+ state.TimeModified = types.StringValue(idpConfig.TimeModified.String())
+ state.AcsUrl = types.StringValue(idpConfig.AcsUrl)
+ state.Description = types.StringValue(idpConfig.Description)
+ state.GroupAttributeName = types.StringValue(idpConfig.GroupAttributeName)
+ state.IdpEntityId = types.StringValue(idpConfig.IdpEntityId)
+ state.Name = types.StringValue(string(idpConfig.Name))
+ state.SloUrl = types.StringValue(idpConfig.SloUrl)
+ state.SpClientId = types.StringValue(idpConfig.SpClientId)
+ state.TechnicalContactEmail = types.StringValue(idpConfig.TechnicalContactEmail)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+// Update is purposefully unsupported since there's no upstream API to update a
+// silo's SAML identity provider configuration.
+//
+// An error is added to the diagnostics so that Terraform will stop execution
+// and prompt the user to fix their configuration.
+func (r *siloSamlIdentityProvider) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ resp.Diagnostics.AddError(
+ "The oxide_silo_saml_identity_provider resource does not support updates.",
+ "This resource represents immutable silo SAML identity provider configuration. Please update your Terraform configuration to match the state.",
+ )
+}
+
+// Delete is purposefully unsupported since there's no upstream API to delete a
+// silo's SAML identity provider configuration.
+//
+// A warning is added to the diagnostics so that the resource will be removed
+// from the state and Terraform execution will continue and prompt the user on
+// what happened.
+func (r *siloSamlIdentityProvider) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ resp.Diagnostics.AddWarning(
+ "The oxide_silo_saml_identity_provider resource does not support deletion.",
+ "This resource represents immutable silo SAML identity provider configuration. The resource will be removed from Terraform state but not from Oxide.",
+ )
+}
diff --git a/internal/provider/resource_silo_saml_identity_provider_test.go b/internal/provider/resource_silo_saml_identity_provider_test.go
new file mode 100644
index 00000000..4bf4e6be
--- /dev/null
+++ b/internal/provider/resource_silo_saml_identity_provider_test.go
@@ -0,0 +1,192 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package provider
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+ "github.com/oxidecomputer/oxide.go/oxide"
+)
+
+type resourceSiloIdentifyProviderConfig struct {
+ SiloBlockName string
+ SiloDNSName string
+ SiloName string
+ SiloSamlIdentityProviderBlockName string
+ SiloSamlIdentityProviderName string
+}
+
+var resourceSiloIdentityProviderConfigTpl = `
+resource "tls_private_key" "self-signed" {
+ algorithm = "RSA"
+ rsa_bits = 2048
+}
+
+resource "tls_self_signed_cert" "self-signed" {
+ private_key_pem = tls_private_key.self-signed.private_key_pem
+ validity_period_hours = 8760
+
+ subject {
+ common_name = "{{.SiloDNSName}}"
+ organization = "Oxide Computer Company"
+ }
+
+ dns_names = ["{{.SiloDNSName}}"]
+
+ allowed_uses = [
+ "key_encipherment",
+ "digital_signature",
+ "server_auth",
+ ]
+}
+
+resource "oxide_silo" "{{.SiloBlockName}}" {
+ name = "{{.SiloName}}"
+ description = "Managed by Terraform."
+ discoverable = true
+ identity_mode = "saml_jit"
+ admin_group_name = "admin"
+
+ quotas = {
+ cpus = 2
+ memory = 137438953472
+ storage = 549755813888
+ }
+
+ mapped_fleet_roles = {
+ admin = ["admin", "collaborator"]
+ viewer = ["viewer"]
+ }
+
+ tls_certificates = [
+ {
+ name = "self-signed-wildcard"
+ description = "Self-signed wildcard certificate for {{.SiloDNSName}}"
+ cert = tls_self_signed_cert.self-signed.cert_pem
+ key = tls_private_key.self-signed.private_key_pem
+ service = "external_api"
+ },
+ ]
+}
+
+resource "oxide_silo_saml_identity_provider" "{{.SiloSamlIdentityProviderBlockName}}" {
+ silo = oxide_silo.{{.SiloBlockName}}.id
+ name = "{{.SiloSamlIdentityProviderName}}"
+ description = "Managed by Terraform."
+ group_attribute_name = "example"
+ idp_entity_id = "example"
+ acs_url = "https://example.com"
+ slo_url = "https://example.com"
+ sp_client_id = "example"
+ technical_contact_email = "example@example.com"
+
+ idp_metadata_source = {
+ type = "base64_encoded_xml"
+ data = "PG1kOkVudGl0eURlc2NyaXB0b3IKCXhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bWV0YWRhdGEiCgl4bWxuczptZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm1ldGFkYXRhIgoJeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIKCXhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIiBlbnRpdHlJRD0iaHR0cHM6Ly9leGFtcGxlLmNvbSI+Cgk8bWQ6SURQU1NPRGVzY3JpcHRvciBXYW50QXV0aG5SZXF1ZXN0c1NpZ25lZD0idHJ1ZSIgcHJvdG9jb2xTdXBwb3J0RW51bWVyYXRpb249InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+CgkJPG1kOktleURlc2NyaXB0b3IgdXNlPSJzaWduaW5nIj4KCQkJPGRzOktleUluZm8+CgkJCQk8ZHM6S2V5TmFtZT5xWlc3N3I2Vy1NQVhCQURPWkdfb0lVeGlWSmhzWHJGa2tEUlFlQWREYzhjPC9kczpLZXlOYW1lPgoJCQkJPGRzOlg1MDlEYXRhPgoJCQkJCTxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ3VUQ0NBYUVDQmdHVUdOYUluekFOQmdrcWhraUc5dzBCQVFzRkFEQWdNUjR3SEFZRFZRUUREQlZrWlcxdkxUQXdaakF6WWpoaFpEUmpaVFExT0RJd0hoY05NalF4TWpNd01UZ3pNREF3V2hjTk16UXhNak13TVRnek1UUXdXakFnTVI0d0hBWURWUVFEREJWa1pXMXZMVEF3WmpBellqaGhaRFJqWlRRMU9ESXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDc04yR2Y4Z040b0hHSVI3NXZTaDBZakc0ejFyNytLSGx2cG84QnZmRm9hVk5QR255NHNOMVJGdlI5V25pOVMvM0lXRHNjaDV3NTZnMnk3MFNYcmloUWVKZUptUHhucVd1cUFuSDVLeUgxcjFZeVNqK3pHRGJpRHJyM3pBNlYvdFErUHlJZ0R1cUEvaGg1cmxoRndwOEdQRndnZFBCNTEyK2x5MmR5bTkzQ1BrdDdTdk1KQXhlOHFWYWZPTU9nVEIzcUdiT25jSDdYd1BMMnlhTUhKYUlsTFMwSHh5Ti81S1RrUk5aZERwb25JTFYvajlZT2hZSDdJRDl3c3Y2dlR2NnM3Y3BST0dPMmdFOUVPM1pzTTlwUWlxMjN0RGlTUjloY3BvT2piOElyc0VzMXYxZDlkUGRTV2xybSt4L0U1THlZb1VVT1RUdnlpWDdrU0dPVVFMbS9BZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFHTGhBSXlITURVSk9QNkttNzRIYjhUSncxdVh4bVJXRjBRcDBza1BtcUxFSEdRYVpic0R4YVBNYzR0ZDI2TUY0R2dMZ2FNZXVnQlkwZk12d0ErMGJDR0EwY2hLVWpJQUwybEg0UGpzVG15cGliVWMySDFlU1BsOTNoYlBzaTFPSm85bTRvblVHcmg3Z3hHWWJ5Tm85R0tBemgvMmZyZVNRbE82K1pmZkdmSlhabEtTT3pnangzcUVYOTc1N2t1Q3VYWVdtQ3hGbmROd0h2ZjdOTVJENUlQOHRYeEN4a09sbUFBZjRKbUlBbnNsNVp1KzR6K0NuZE1vNkYxMjFsT0t4Tkt2Y0ZYaHFQaHJyd1krZlFreEpXdWprVGp2dTRTN1FsM0dOMzVDWURZdDg5a2drL212VmVCaHVRd1pBR3dWRzE3RnlsN3BNRlFyTGtUQ2ErQmJyY1U9PC9kczpYNTA5Q2VydGlmaWNhdGU+CgkJCQk8L2RzOlg1MDlEYXRhPgoJCQk8L2RzOktleUluZm8+CgkJPC9tZDpLZXlEZXNjcmlwdG9yPgoJCTxtZDpBcnRpZmFjdFJlc29sdXRpb25TZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpTT0FQIiBMb2NhdGlvbj0iaHR0cHM6Ly9leGFtcGxlLmNvbSIgaW5kZXg9IjAiLz4KCQk8bWQ6U2luZ2xlTG9nb3V0U2VydmljZSBCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1QT1NUIiBMb2NhdGlvbj0iaHR0cHM6Ly9leGFtcGxlLmNvbSIvPgoJCTxtZDpTaW5nbGVMb2dvdXRTZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLVJlZGlyZWN0IiBMb2NhdGlvbj0iaHR0cHM6Ly9leGFtcGxlLmNvbSIvPgoJCTxtZDpTaW5nbGVMb2dvdXRTZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpIVFRQLUFydGlmYWN0IiBMb2NhdGlvbj0iaHR0cHM6Ly9leGFtcGxlLmNvbSIvPgoJCTxtZDpTaW5nbGVMb2dvdXRTZXJ2aWNlIEJpbmRpbmc9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpiaW5kaW5nczpTT0FQIiBMb2NhdGlvbj0iaHR0cHM6Ly9leGFtcGxlLmNvbSIvPgoJCTxtZDpOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6cGVyc2lzdGVudDwvbWQ6TmFtZUlERm9ybWF0PgoJCTxtZDpOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50PC9tZDpOYW1lSURGb3JtYXQ+CgkJPG1kOk5hbWVJREZvcm1hdD51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZDwvbWQ6TmFtZUlERm9ybWF0PgoJCTxtZDpOYW1lSURGb3JtYXQ+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzPC9tZDpOYW1lSURGb3JtYXQ+CgkJPG1kOlNpbmdsZVNpZ25PblNlcnZpY2UgQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgTG9jYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20iLz4KCQk8bWQ6U2luZ2xlU2lnbk9uU2VydmljZSBCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1SZWRpcmVjdCIgTG9jYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20iLz4KCQk8bWQ6U2luZ2xlU2lnbk9uU2VydmljZSBCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6U09BUCIgTG9jYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20iLz4KCQk8bWQ6U2luZ2xlU2lnbk9uU2VydmljZSBCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1BcnRpZmFjdCIgTG9jYXRpb249Imh0dHBzOi8vZXhhbXBsZS5jb20iLz4KCTwvbWQ6SURQU1NPRGVzY3JpcHRvcj4KPC9tZDpFbnRpdHlEZXNjcmlwdG9yPgo="
+ }
+}
+`
+
+func TestAccSiloResourceSiloSamlIdentityProvider_full(t *testing.T) {
+ siloBlockName := newBlockName("silo")
+ siloName := newResourceName()
+ siloSamlIdentityProviderBlockName := newBlockName("silo-idp")
+ siloSamlIdentityProviderName := newResourceName()
+
+ siloSamlIdentityProviderResourceID := fmt.Sprintf("oxide_silo_saml_identity_provider.%s", siloSamlIdentityProviderBlockName)
+
+ siloDNSName := os.Getenv("OXIDE_SILO_DNS_NAME")
+ if siloDNSName == "" {
+ t.Skip("Skipping test. Export OXIDE_SILO_DNS_NAME to run.")
+ }
+
+ config, err := parsedAccConfig(
+ resourceSiloIdentifyProviderConfig{
+ SiloBlockName: siloBlockName,
+ SiloDNSName: siloDNSName,
+ SiloName: siloName,
+ SiloSamlIdentityProviderBlockName: siloSamlIdentityProviderBlockName,
+ SiloSamlIdentityProviderName: siloSamlIdentityProviderName,
+ },
+ resourceSiloIdentityProviderConfigTpl,
+ )
+ if err != nil {
+ t.Errorf("error parsing config template data: %e", err)
+ }
+
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(),
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "tls": {
+ Source: "hashicorp/tls",
+ },
+ },
+ CheckDestroy: testAccSiloSamlIdentityProviderDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: config,
+ Check: checkResourceSiloSamlIdentityProvider(siloSamlIdentityProviderResourceID, siloSamlIdentityProviderName),
+ },
+ },
+ })
+}
+
+func checkResourceSiloSamlIdentityProvider(resourceID string, nameAttr string) resource.TestCheckFunc {
+ return resource.ComposeAggregateTestCheckFunc([]resource.TestCheckFunc{
+ resource.TestCheckResourceAttrSet(resourceID, "id"),
+ resource.TestCheckResourceAttr(resourceID, "name", nameAttr),
+ resource.TestCheckResourceAttr(resourceID, "description", "Managed by Terraform."),
+ resource.TestCheckResourceAttrSet(resourceID, "silo"),
+ resource.TestCheckResourceAttr(resourceID, "group_attribute_name", "example"),
+ resource.TestCheckResourceAttr(resourceID, "idp_entity_id", "example"),
+ resource.TestCheckResourceAttr(resourceID, "acs_url", "https://example.com"),
+ resource.TestCheckResourceAttr(resourceID, "slo_url", "https://example.com"),
+ resource.TestCheckResourceAttr(resourceID, "sp_client_id", "example"),
+ resource.TestCheckResourceAttr(resourceID, "technical_contact_email", "example@example.com"),
+ resource.TestCheckResourceAttr(resourceID, "idp_metadata_source.type", "base64_encoded_xml"),
+ resource.TestCheckResourceAttrSet(resourceID, "idp_metadata_source.data"),
+ }...)
+}
+
+func testAccSiloSamlIdentityProviderDestroy(s *terraform.State) error {
+ client, err := newTestClient()
+ if err != nil {
+ return err
+ }
+
+ for _, rs := range s.RootModule().Resources {
+ if rs.Type != "oxide_silo_saml_identity_provider" {
+ continue
+ }
+
+ ctx := context.Background()
+ ctx, cancel := context.WithTimeout(ctx, time.Minute)
+ defer cancel()
+
+ params := oxide.SamlIdentityProviderViewParams{
+ Provider: oxide.NameOrId(rs.Primary.Attributes["id"]),
+ }
+
+ res, err := client.SamlIdentityProviderView(ctx, params)
+ if err != nil && is404(err) {
+ continue
+ }
+
+ return fmt.Errorf("silo saml identity provider (%v) still exists", &res.Name)
+ }
+
+ return nil
+}
diff --git a/internal/provider/resource_silo_test.go b/internal/provider/resource_silo_test.go
index 202ceb7a..d8c6f4fe 100644
--- a/internal/provider/resource_silo_test.go
+++ b/internal/provider/resource_silo_test.go
@@ -68,7 +68,7 @@ resource "oxide_silo" "{{.BlockName}}" {
name = "self-signed-wildcard"
description = "Self-signed wildcard certificate for *.sys.r3.oxide-preview.com."
cert = tls_self_signed_cert.self-signed.cert_pem
- key = tls_private_key.self-signed.private_key_pem
+ key = tls_private_key.self-signed.private_key_pem
service = "external_api"
},
]
@@ -128,7 +128,7 @@ resource "oxide_silo" "{{.BlockName}}" {
name = "self-signed-wildcard"
description = "Self-signed wildcard certificate for *.sys.r3.oxide-preview.com."
cert = tls_self_signed_cert.self-signed.cert_pem
- key = tls_private_key.self-signed.private_key_pem
+ key = tls_private_key.self-signed.private_key_pem
service = "external_api"
},
]