Skip to content

Commit 40ee907

Browse files
committed
oxide_silo_idp_configuration: initial resource
1 parent 7478329 commit 40ee907

File tree

2 files changed

+361
-0
lines changed

2 files changed

+361
-0
lines changed

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,6 @@ func (p *oxideProvider) Resources(_ context.Context) []func() resource.Resource
199199
NewVPCSubnetResource,
200200
NewFloatingIPResource,
201201
NewSiloResource,
202+
NewSiloIdpConfigurationResource,
202203
}
203204
}
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
package provider
6+
7+
import (
8+
"context"
9+
"fmt"
10+
11+
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
12+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
14+
"github.com/hashicorp/terraform-plugin-framework/resource"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
19+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
20+
"github.com/hashicorp/terraform-plugin-framework/types"
21+
"github.com/hashicorp/terraform-plugin-log/tflog"
22+
"github.com/oxidecomputer/oxide.go/oxide"
23+
)
24+
25+
var (
26+
_ resource.Resource = (*siloIdpConfigurationResource)(nil)
27+
_ resource.ResourceWithConfigure = (*siloIdpConfigurationResource)(nil)
28+
_ resource.ResourceWithImportState = (*siloIdpConfigurationResource)(nil)
29+
)
30+
31+
func NewSiloIdpConfigurationResource() resource.Resource {
32+
return &siloIdpConfigurationResource{}
33+
}
34+
35+
type siloIdpConfigurationResource struct {
36+
client *oxide.Client
37+
}
38+
39+
type siloIdpConfigurationResourceModel struct {
40+
ID types.String `tfsdk:"id"`
41+
Name types.String `tfsdk:"name"`
42+
Description types.String `tfsdk:"description"`
43+
Silo types.String `tfsdk:"silo"`
44+
AcsUrl types.String `tfsdk:"acs_url"`
45+
IdpEntityId types.String `tfsdk:"idp_entity_id"`
46+
SloUrl types.String `tfsdk:"slo_url"`
47+
SpClientId types.String `tfsdk:"sp_client_id"`
48+
TechnicalContactEmail types.String `tfsdk:"technical_contact_email"`
49+
IdpMetadataSource *siloIdpConfigurationResourceMetadataSourceModel `tfsdk:"idp_metadata_source"`
50+
GroupAttributeName types.String `tfsdk:"group_attribute_name"`
51+
SigningKeypair *siloIdpConfigurationResourceSigningKeypairModel `tfsdk:"signing_keypair"`
52+
TimeCreated types.String `tfsdk:"time_created"`
53+
TimeModified types.String `tfsdk:"time_modified"`
54+
Timeouts timeouts.Value `tfsdk:"timeouts"`
55+
}
56+
57+
type siloIdpConfigurationResourceSigningKeypairModel struct {
58+
PrivateKey types.String `tfsdk:"private_key"`
59+
PublicCert types.String `tfsdk:"public_cert"`
60+
}
61+
62+
type siloIdpConfigurationResourceMetadataSourceModel struct {
63+
Type types.String `tfsdk:"type"`
64+
Url types.String `tfsdk:"url"`
65+
Data types.String `tfsdk:"data"`
66+
}
67+
68+
func (r *siloIdpConfigurationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
69+
resp.TypeName = "oxide_silo_idp_configuration"
70+
}
71+
72+
func (r *siloIdpConfigurationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
73+
if req.ProviderData == nil {
74+
return
75+
}
76+
77+
r.client = req.ProviderData.(*oxide.Client)
78+
}
79+
80+
func (r *siloIdpConfigurationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
81+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
82+
}
83+
84+
func (r *siloIdpConfigurationResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
85+
resp.Schema = schema.Schema{
86+
Attributes: map[string]schema.Attribute{
87+
"silo": schema.StringAttribute{
88+
Required: true,
89+
Description: "Name or ID of the silo.",
90+
PlanModifiers: []planmodifier.String{
91+
stringplanmodifier.RequiresReplace(),
92+
},
93+
},
94+
"acs_url": schema.StringAttribute{
95+
Required: true,
96+
Description: "Service provider endpoint for response.",
97+
PlanModifiers: []planmodifier.String{
98+
stringplanmodifier.RequiresReplace(),
99+
},
100+
},
101+
"description": schema.StringAttribute{
102+
Required: true,
103+
Description: "Free-form text about this resource.",
104+
PlanModifiers: []planmodifier.String{
105+
stringplanmodifier.RequiresReplace(),
106+
},
107+
},
108+
"group_attribute_name": schema.StringAttribute{
109+
Optional: true,
110+
Description: "SAML attribute for group membership.",
111+
PlanModifiers: []planmodifier.String{
112+
stringplanmodifier.RequiresReplace(),
113+
},
114+
},
115+
"id": schema.StringAttribute{
116+
Computed: true,
117+
Description: "Unique, immutable, system-controlled identifier of the SAML identity provider.",
118+
PlanModifiers: []planmodifier.String{
119+
stringplanmodifier.UseStateForUnknown(),
120+
},
121+
},
122+
"idp_entity_id": schema.StringAttribute{
123+
Required: true,
124+
Description: "Identity provider's entity ID.",
125+
PlanModifiers: []planmodifier.String{
126+
stringplanmodifier.RequiresReplace(),
127+
},
128+
},
129+
// TODO: Validate that `url` and `data` are not set simuntaneously.
130+
"idp_metadata_source": schema.SingleNestedAttribute{
131+
Required: true,
132+
Description: "Source of identity provider metadata (URL or base64-encoded XML).",
133+
Attributes: map[string]schema.Attribute{
134+
"type": schema.StringAttribute{
135+
Required: true,
136+
Validators: []validator.String{
137+
stringvalidator.OneOf(
138+
"url",
139+
"base64_encoded_xml",
140+
),
141+
},
142+
},
143+
"url": schema.StringAttribute{
144+
Optional: true,
145+
Description: "URL to fetch metadata from (required when type is 'url').",
146+
},
147+
"data": schema.StringAttribute{
148+
Optional: true,
149+
Description: "Base64-encoded XML metadata (required when type is 'base64_encoded_xml').",
150+
},
151+
},
152+
PlanModifiers: []planmodifier.Object{
153+
objectplanmodifier.RequiresReplace(),
154+
},
155+
},
156+
"name": schema.StringAttribute{
157+
Required: true,
158+
Description: "Unique, immutable, user-controlled identifier of the SAML identity provider.",
159+
PlanModifiers: []planmodifier.String{
160+
stringplanmodifier.RequiresReplace(),
161+
},
162+
Validators: []validator.String{
163+
stringvalidator.LengthAtMost(63),
164+
},
165+
},
166+
"signing_keypair": schema.SingleNestedAttribute{
167+
Optional: true,
168+
Description: "RSA private key and public certificate for signing.",
169+
Attributes: map[string]schema.Attribute{
170+
"private_key": schema.StringAttribute{
171+
Required: true,
172+
Sensitive: true,
173+
Description: "RSA private key (base64 encoded).",
174+
},
175+
"public_cert": schema.StringAttribute{
176+
Required: true,
177+
Description: "Public certificate (base64 encoded).",
178+
},
179+
},
180+
PlanModifiers: []planmodifier.Object{
181+
objectplanmodifier.RequiresReplace(),
182+
},
183+
},
184+
"slo_url": schema.StringAttribute{
185+
Required: true,
186+
Description: "Service provider endpoint for logout requests.",
187+
PlanModifiers: []planmodifier.String{
188+
stringplanmodifier.RequiresReplace(),
189+
},
190+
},
191+
"sp_client_id": schema.StringAttribute{
192+
Required: true,
193+
Description: "Service provider's client ID.",
194+
PlanModifiers: []planmodifier.String{
195+
stringplanmodifier.RequiresReplace(),
196+
},
197+
},
198+
"technical_contact_email": schema.StringAttribute{
199+
Required: true,
200+
Description: "Customer's technical contact email.",
201+
PlanModifiers: []planmodifier.String{
202+
stringplanmodifier.RequiresReplace(),
203+
},
204+
},
205+
"time_created": schema.StringAttribute{
206+
Computed: true,
207+
Description: "Timestamp of when this SAML identity provider was created.",
208+
},
209+
"time_modified": schema.StringAttribute{
210+
Computed: true,
211+
Description: "Timestamp of when this SAML identity provider was last modified.",
212+
},
213+
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
214+
Create: true,
215+
Read: true,
216+
}),
217+
},
218+
}
219+
}
220+
221+
func (r *siloIdpConfigurationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
222+
var plan siloIdpConfigurationResourceModel
223+
224+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
225+
if resp.Diagnostics.HasError() {
226+
return
227+
}
228+
229+
createTimeout, diags := plan.Timeouts.Create(ctx, defaultTimeout())
230+
resp.Diagnostics.Append(diags...)
231+
if resp.Diagnostics.HasError() {
232+
return
233+
}
234+
235+
ctx, cancel := context.WithTimeout(ctx, createTimeout)
236+
defer cancel()
237+
238+
idpMetadataSource := oxide.IdpMetadataSource{
239+
Type: oxide.IdpMetadataSourceType(plan.IdpMetadataSource.Type.ValueString()),
240+
}
241+
242+
switch idpMetadataSource.Type {
243+
case oxide.IdpMetadataSourceTypeBase64EncodedXml:
244+
idpMetadataSource.Data = plan.IdpMetadataSource.Data.ValueString()
245+
case oxide.IdpMetadataSourceTypeUrl:
246+
idpMetadataSource.Url = plan.IdpMetadataSource.Url.ValueString()
247+
}
248+
249+
params := oxide.SamlIdentityProviderCreateParams{
250+
Silo: oxide.NameOrId(plan.Silo.ValueString()),
251+
Body: &oxide.SamlIdentityProviderCreate{
252+
AcsUrl: plan.AcsUrl.ValueString(),
253+
IdpEntityId: plan.IdpEntityId.ValueString(),
254+
Name: oxide.Name(plan.Name.ValueString()),
255+
SloUrl: plan.SloUrl.ValueString(),
256+
SpClientId: plan.SpClientId.ValueString(),
257+
TechnicalContactEmail: plan.TechnicalContactEmail.ValueString(),
258+
IdpMetadataSource: idpMetadataSource,
259+
Description: plan.Description.ValueString(),
260+
GroupAttributeName: plan.GroupAttributeName.ValueString(),
261+
},
262+
}
263+
264+
if plan.SigningKeypair != nil {
265+
params.Body.SigningKeypair = oxide.DerEncodedKeyPair{
266+
PrivateKey: plan.SigningKeypair.PrivateKey.ValueString(),
267+
PublicCert: plan.SigningKeypair.PublicCert.ValueString(),
268+
}
269+
}
270+
271+
idpConfig, err := r.client.SamlIdentityProviderCreate(ctx, params)
272+
if err != nil {
273+
resp.Diagnostics.AddError(
274+
"Error creating SAML identity provider",
275+
"API error: "+err.Error(),
276+
)
277+
return
278+
}
279+
280+
tflog.Trace(ctx, fmt.Sprintf("created SAML identity provider with ID: %v", idpConfig.Id), map[string]any{"success": true})
281+
282+
plan.ID = types.StringValue(idpConfig.Id)
283+
plan.TimeCreated = types.StringValue(idpConfig.TimeCreated.String())
284+
plan.TimeModified = types.StringValue(idpConfig.TimeModified.String())
285+
286+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
287+
if resp.Diagnostics.HasError() {
288+
return
289+
}
290+
}
291+
292+
func (r *siloIdpConfigurationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
293+
var state siloIdpConfigurationResourceModel
294+
295+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
296+
if resp.Diagnostics.HasError() {
297+
return
298+
}
299+
300+
readTimeout, diags := state.Timeouts.Read(ctx, defaultTimeout())
301+
resp.Diagnostics.Append(diags...)
302+
if resp.Diagnostics.HasError() {
303+
return
304+
}
305+
306+
ctx, cancel := context.WithTimeout(ctx, readTimeout)
307+
defer cancel()
308+
309+
params := oxide.SamlIdentityProviderViewParams{
310+
Provider: oxide.NameOrId(state.ID.ValueString()),
311+
}
312+
313+
idpConfig, err := r.client.SamlIdentityProviderView(ctx, params)
314+
if err != nil {
315+
if is404(err) {
316+
resp.State.RemoveResource(ctx)
317+
return
318+
}
319+
resp.Diagnostics.AddError(
320+
"Unable to read SAML identity provider:",
321+
"API error: "+err.Error(),
322+
)
323+
return
324+
}
325+
326+
tflog.Trace(ctx, fmt.Sprintf("read SAML identity provider with ID: %v", idpConfig.Id), map[string]any{"success": true})
327+
328+
state.ID = types.StringValue(idpConfig.Id)
329+
state.Name = types.StringValue(string(idpConfig.Name))
330+
state.AcsUrl = types.StringValue(idpConfig.AcsUrl)
331+
state.IdpEntityId = types.StringValue(idpConfig.IdpEntityId)
332+
state.SloUrl = types.StringValue(idpConfig.SloUrl)
333+
state.SpClientId = types.StringValue(idpConfig.SpClientId)
334+
state.TechnicalContactEmail = types.StringValue(idpConfig.TechnicalContactEmail)
335+
state.TimeCreated = types.StringValue(idpConfig.TimeCreated.String())
336+
state.TimeModified = types.StringValue(idpConfig.TimeModified.String())
337+
state.Description = types.StringValue(idpConfig.Description)
338+
state.GroupAttributeName = types.StringValue(idpConfig.GroupAttributeName)
339+
340+
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
341+
if resp.Diagnostics.HasError() {
342+
return
343+
}
344+
}
345+
346+
func (r *siloIdpConfigurationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
347+
resp.Diagnostics.AddError(
348+
"Update Not Supported",
349+
"The oxide_silo_idp_configuration resource does not support updates. "+
350+
"Changes require replacement of the resource.",
351+
)
352+
}
353+
354+
func (r *siloIdpConfigurationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
355+
resp.Diagnostics.AddError(
356+
"Delete Not Supported",
357+
"The oxide_silo_idp_configuration resource does not support deletion. "+
358+
"This resource represents immutable SAML identity provider configuration.",
359+
)
360+
}

0 commit comments

Comments
 (0)