From 0d30e1c04431a569ca741e1656e2fad5f1af35d4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 13 Sep 2024 20:54:54 +0800 Subject: [PATCH 1/3] feat: added identity oidc auth resource --- docs/resources/identity_oidc_auth.md | 80 ++++ .../infisical_identity_oidc_auth/resource.tf | 33 ++ internal/client/identity_oidc_auth.go | 91 ++++ internal/client/model.go | 117 +++-- internal/provider/provider.go | 1 + .../provider/resource/identity_oidc_auth.go | 406 ++++++++++++++++++ 6 files changed, 704 insertions(+), 24 deletions(-) create mode 100644 docs/resources/identity_oidc_auth.md create mode 100644 examples/resources/infisical_identity_oidc_auth/resource.tf create mode 100644 internal/client/identity_oidc_auth.go create mode 100644 internal/provider/resource/identity_oidc_auth.go diff --git a/docs/resources/identity_oidc_auth.md b/docs/resources/identity_oidc_auth.md new file mode 100644 index 0000000..71fa247 --- /dev/null +++ b/docs/resources/identity_oidc_auth.md @@ -0,0 +1,80 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_identity_oidc_auth Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create and manage identity oidc auth in Infisical. +--- + +# infisical_identity_oidc_auth (Resource) + +Create and manage identity oidc auth in Infisical. + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_identity" "machine-identity-1" { + name = "machine-identity-1" + role = "admin" + org_id = "<>" +} + +resource "infisical_identity_oidc_auth" "oidc-auth" { + identity_id = infisical_identity.machine-identity-1.id + oidc_discovery_url = "<>" + bound_issuer = "<>" + bound_audiences = ["sample-audience"] + bound_subject = "<>" +} +``` + + +## Schema + +### Required + +- `bound_issuer` (String) The unique identifier of the identity provider issuing the OIDC tokens. +- `identity_id` (String) The ID of the identity to attach the configuration onto. +- `oidc_discovery_url` (String) The URL used to retrieve the OpenID Connect configuration from the identity provider. + +### Optional + +- `access_token_max_ttl` (Number) The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000 +- `access_token_num_uses_limit` (Number) The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. Default:0 +- `access_token_trusted_ips` (Attributes List) A list of IPs or CIDR ranges that access tokens can be used from. You can use 0.0.0.0/0, to allow usage from any network address... (see [below for nested schema](#nestedatt--access_token_trusted_ips)) +- `access_token_ttl` (Number) The lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000 +- `bound_audiences` (List of String) The comma-separated list of intended recipients. +- `bound_claims` (Map of String) The attributes that should be present in the JWT for it to be valid. The provided values can be a glob pattern. +- `bound_subject` (String) The expected principal that is the subject of the JWT. +- `oidc_ca_certificate` (String) The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints + +### Read-Only + +- `id` (String) The ID of the oidc auth. + + +### Nested Schema for `access_token_trusted_ips` + +Optional: + +- `ip_address` (String) diff --git a/examples/resources/infisical_identity_oidc_auth/resource.tf b/examples/resources/infisical_identity_oidc_auth/resource.tf new file mode 100644 index 0000000..c930d7b --- /dev/null +++ b/examples/resources/infisical_identity_oidc_auth/resource.tf @@ -0,0 +1,33 @@ +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + client_id = "<>" + client_secret = "<>" +} + +resource "infisical_project" "example" { + name = "example" + slug = "example" +} + +resource "infisical_identity" "machine-identity-1" { + name = "machine-identity-1" + role = "admin" + org_id = "<>" +} + +resource "infisical_identity_oidc_auth" "oidc-auth" { + identity_id = infisical_identity.machine-identity-1.id + oidc_discovery_url = "<>" + bound_issuer = "<>" + bound_audiences = ["sample-audience"] + bound_subject = "<>" +} diff --git a/internal/client/identity_oidc_auth.go b/internal/client/identity_oidc_auth.go new file mode 100644 index 0000000..dd2d873 --- /dev/null +++ b/internal/client/identity_oidc_auth.go @@ -0,0 +1,91 @@ +package infisicalclient + +import ( + "fmt" + "net/http" +) + +func (client Client) CreateIdentityOidcAuth(request CreateIdentityOidcAuthRequest) (IdentityOidcAuth, error) { + var body CreateIdentityOidcAuthResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&body). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post("api/v1/auth/oidc-auth/identities/" + request.IdentityID) + + if err != nil { + return IdentityOidcAuth{}, fmt.Errorf("CreateIdentityOidcAuth: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return IdentityOidcAuth{}, fmt.Errorf("CreateIdentityOidcAuth: Unsuccessful response. [response=%s]", string(response.Body())) + } + + return body.IdentityOidcAuth, nil +} + +func (client Client) GetIdentityOidcAuth(request GetIdentityOidcAuthRequest) (IdentityOidcAuth, error) { + var body GetIdentityOidcAuthResponse + + httpRequest := client.Config.HttpClient. + R(). + SetResult(&body). + SetHeader("User-Agent", USER_AGENT) + + response, err := httpRequest.Get("api/v1/auth/oidc-auth/identities/" + request.IdentityID) + + if response.StatusCode() == http.StatusNotFound { + return IdentityOidcAuth{}, ErrNotFound + } + + if err != nil { + return IdentityOidcAuth{}, fmt.Errorf("GetIdentityOidcAuth: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return IdentityOidcAuth{}, fmt.Errorf("GetIdentityOidcAuth: Unsuccessful response. [response=%v]", string(response.Body())) + } + + return body.IdentityOidcAuth, nil +} + +func (client Client) UpdateIdentityOidcAuth(request UpdateIdentityOidcAuthRequest) (IdentityOidcAuth, error) { + var body UpdateIdentityOidcAuthResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&body). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch("api/v1/auth/oidc-auth/identities/" + request.IdentityID) + + if err != nil { + return IdentityOidcAuth{}, fmt.Errorf("UpdateIdentityOidcAuth: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return IdentityOidcAuth{}, fmt.Errorf("UpdateIdentityOidcAuth: Unsuccessful response. [response=%s]", string(response.Body())) + } + + return body.IdentityOidcAuth, nil +} + +func (client Client) RevokeIdentityOidcAuth(request RevokeIdentityOidcAuthRequest) (IdentityOidcAuth, error) { + var body RevokeIdentityOidcAuthResponse + response, err := client.Config.HttpClient. + R(). + SetResult(&body). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete("api/v1/auth/oidc-auth/identities/" + request.IdentityID) + + if err != nil { + return IdentityOidcAuth{}, fmt.Errorf("RevokeIdentityOidcAuth: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return IdentityOidcAuth{}, fmt.Errorf("RevokeIdentityOidcAuth: Unsuccessful response. [response=%s]", string(response.Body())) + } + + return body.IdentityOidcAuth, nil +} diff --git a/internal/client/model.go b/internal/client/model.go index a6184e5..bdd0464 100644 --- a/internal/client/model.go +++ b/internal/client/model.go @@ -191,6 +191,23 @@ type IdentityKubernetesAuth struct { TokenReviewerJwt string `json:"tokenReviewerJwt"` } +type IdentityOidcAuth struct { + ID string `json:"id"` + AccessTokenTTL int64 `json:"accessTokenTTL"` + AccessTokenMaxTTL int64 `json:"accessTokenMaxTTL"` + AccessTokenNumUsesLimit int64 `json:"accessTokenNumUsesLimit"` + AccessTokenTrustedIPS []IdentityAuthTrustedIp `json:"accessTokenTrustedIps"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + IdentityID string `json:"identityId"` + OidcDiscoveryUrl string `json:"oidcDiscoveryUrl"` + BoundIssuer string `json:"boundIssuer"` + BoundAudiences string `json:"boundAudiences"` + BoundClaims map[string]string `json:"boundClaims"` + BoundSubject string `json:"boundSubject"` + CACERT string `json:"caCert"` +} + type IdentityAuthTrustedIp struct { Type string `json:"type"` Prefix *int `json:"prefix,omitempty"` @@ -367,21 +384,21 @@ type EncryptedSecret struct { // create secrets. type CreateSecretV3Request struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - SecretPath string `json:"secretPath"` - TagIDs []string `json:"tags"` + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + SecretPath string `json:"secretPath"` + TagIDs []string `json:"tags"` } // delete secret by name api. @@ -395,15 +412,15 @@ type DeleteSecretV3Request struct { // update secret by name api. type UpdateSecretByNameV3Request struct { - SecretName string `json:"secretName"` - WorkspaceID string `json:"workspaceId"` - Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - TagIDs []string `json:"tags,omitempty"` + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + TagIDs []string `json:"tags,omitempty"` } // get secret by name api. @@ -1196,6 +1213,58 @@ type UpdateIdentityKubernetesAuthRequest struct { AccessTokenNumUsesLimit int64 `json:"accessTokenNumUsesLimit,omitempty"` } +type CreateIdentityOidcAuthResponse struct { + IdentityOidcAuth IdentityOidcAuth `json:"identityOidcAuth"` +} + +type UpdateIdentityOidcAuthResponse struct { + IdentityOidcAuth IdentityOidcAuth `json:"identityOidcAuth"` +} + +type GetIdentityOidcAuthResponse struct { + IdentityOidcAuth IdentityOidcAuth `json:"identityOidcAuth"` +} + +type GetIdentityOidcAuthRequest struct { + IdentityID string `json:"identityId"` +} + +type RevokeIdentityOidcAuthRequest struct { + IdentityID string `json:"identityId"` +} + +type RevokeIdentityOidcAuthResponse struct { + IdentityOidcAuth IdentityOidcAuth `json:"identityOidcAuth"` +} + +type CreateIdentityOidcAuthRequest struct { + IdentityID string `json:"identityId"` + OidcDiscoveryUrl string `json:"oidcDiscoveryUrl"` + CACERT string `json:"caCert"` + BoundIssuer string `json:"boundIssuer"` + BoundAudiences string `json:"boundAudiences"` + BoundClaims map[string]string `json:"boundClaims"` + BoundSubject string `json:"boundSubject"` + AccessTokenTrustedIPS []IdentityAuthTrustedIpRequest `json:"accessTokenTrustedIps,omitempty"` + AccessTokenTTL int64 `json:"accessTokenTTL,omitempty"` + AccessTokenMaxTTL int64 `json:"accessTokenMaxTTL,omitempty"` + AccessTokenNumUsesLimit int64 `json:"accessTokenNumUsesLimit,omitempty"` +} + +type UpdateIdentityOidcAuthRequest struct { + IdentityID string `json:"identityId"` + OidcDiscoveryUrl string `json:"oidcDiscoveryUrl"` + CACERT string `json:"caCert"` + BoundIssuer string `json:"boundIssuer"` + BoundAudiences string `json:"boundAudiences"` + BoundClaims map[string]string `json:"boundClaims"` + BoundSubject string `json:"boundSubject"` + AccessTokenTrustedIPS []IdentityAuthTrustedIpRequest `json:"accessTokenTrustedIps,omitempty"` + AccessTokenTTL int64 `json:"accessTokenTTL,omitempty"` + AccessTokenMaxTTL int64 `json:"accessTokenMaxTTL,omitempty"` + AccessTokenNumUsesLimit int64 `json:"accessTokenNumUsesLimit,omitempty"` +} + type UpdateIdentityKubernetesAuthResponse struct { IdentityKubernetesAuth IdentityKubernetesAuth `json:"identityKubernetesAuth"` } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5ce7fe6..918bc9e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -247,5 +247,6 @@ func (p *infisicalProvider) Resources(_ context.Context) []func() resource.Resou infisicalResource.NewIdentityKubernetesAuthResource, infisicalResource.NewIdentityGcpAuthResource, infisicalResource.NewIdentityAzureAuthResource, + infisicalResource.NewIdentityOidcAuthResource, } } diff --git a/internal/provider/resource/identity_oidc_auth.go b/internal/provider/resource/identity_oidc_auth.go new file mode 100644 index 0000000..6f187da --- /dev/null +++ b/internal/provider/resource/identity_oidc_auth.go @@ -0,0 +1,406 @@ +package resource + +import ( + "context" + "fmt" + "strconv" + "strings" + infisical "terraform-provider-infisical/internal/client" + infisicalclient "terraform-provider-infisical/internal/client" + infisicalstrings "terraform-provider-infisical/internal/pkg/strings" + "terraform-provider-infisical/internal/pkg/terraform" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// NewIdentityOidcAuthResource is a helper function to simplify the provider implementation. +func NewIdentityOidcAuthResource() resource.Resource { + return &IdentityOidcAuthResource{} +} + +// IdentityOidcAuthResource is the resource implementation. +type IdentityOidcAuthResource struct { + client *infisical.Client +} + +// IdentityOidcAuthResourceSourceModel describes the data source data model. +type IdentityOidcAuthResourceModel struct { + ID types.String `tfsdk:"id"` + IdentityID types.String `tfsdk:"identity_id"` + OidcDiscoveryUrl types.String `tfsdk:"oidc_discovery_url"` + CaCertificate types.String `tfsdk:"oidc_ca_certificate"` + BoundIssuer types.String `tfsdk:"bound_issuer"` + BoundAudiences types.List `tfsdk:"bound_audiences"` + BoundClaims types.Map `tfsdk:"bound_claims"` + BoundSubject types.String `tfsdk:"bound_subject"` + AccessTokenTrustedIps types.List `tfsdk:"access_token_trusted_ips"` + AccessTokenTTL types.Int64 `tfsdk:"access_token_ttl"` + AccessTokenMaxTTL types.Int64 `tfsdk:"access_token_max_ttl"` + AccessTokenNumUsesLimit types.Int64 `tfsdk:"access_token_num_uses_limit"` +} + +type IdentityOidcAuthResourceTrustedIps struct { + IpAddress types.String `tfsdk:"ip_address"` +} + +// Metadata returns the resource type name. +func (r *IdentityOidcAuthResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_identity_oidc_auth" +} + +// Schema defines the schema for the resource. +func (r *IdentityOidcAuthResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Create and manage identity oidc auth in Infisical.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the oidc auth.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "identity_id": schema.StringAttribute{ + Description: "The ID of the identity to attach the configuration onto.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "oidc_discovery_url": schema.StringAttribute{ + Description: "The URL used to retrieve the OpenID Connect configuration from the identity provider.", + Required: true, + }, + "bound_issuer": schema.StringAttribute{ + Description: "The unique identifier of the identity provider issuing the OIDC tokens.", + Required: true, + }, + "bound_audiences": schema.ListAttribute{ + Description: "The comma-separated list of intended recipients.", + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + "bound_claims": schema.MapAttribute{ + Description: "The attributes that should be present in the JWT for it to be valid. The provided values can be a glob pattern.", + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + "bound_subject": schema.StringAttribute{ + Description: "The expected principal that is the subject of the JWT.", + Optional: true, + Computed: true, + }, + "oidc_ca_certificate": schema.StringAttribute{ + Description: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints", + MarkdownDescription: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints", + Optional: true, + Computed: true, + }, + "access_token_trusted_ips": schema.ListNestedAttribute{ + Optional: true, + Computed: true, + Description: "A list of IPs or CIDR ranges that access tokens can be used from. You can use 0.0.0.0/0, to allow usage from any network address...", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "ip_address": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + }, + }, + }, + "access_token_ttl": schema.Int64Attribute{ + Description: "The lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000", + Computed: true, + Optional: true, + }, + "access_token_max_ttl": schema.Int64Attribute{ + Description: "The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000", + Computed: true, + Optional: true, + }, + "access_token_num_uses_limit": schema.Int64Attribute{ + Description: "The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. Default:0", + Computed: true, + Optional: true, + }, + }, + } +} + +// Configure adds the provider configured client to the resource. +func (r *IdentityOidcAuthResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*infisical.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func updateOidcAuthStateByApi(ctx context.Context, diagnose diag.Diagnostics, plan *IdentityOidcAuthResourceModel, newIdentityOidcAuth *infisicalclient.IdentityOidcAuth) { + plan.AccessTokenMaxTTL = types.Int64Value(newIdentityOidcAuth.AccessTokenMaxTTL) + plan.AccessTokenTTL = types.Int64Value(newIdentityOidcAuth.AccessTokenTTL) + plan.AccessTokenNumUsesLimit = types.Int64Value(newIdentityOidcAuth.AccessTokenNumUsesLimit) + + plan.OidcDiscoveryUrl = types.StringValue(newIdentityOidcAuth.OidcDiscoveryUrl) + plan.BoundIssuer = types.StringValue(newIdentityOidcAuth.BoundIssuer) + plan.BoundSubject = types.StringValue(newIdentityOidcAuth.BoundSubject) + plan.CaCertificate = types.StringValue(newIdentityOidcAuth.CACERT) + + boundClaimsElements := make(map[string]attr.Value) + for key, value := range newIdentityOidcAuth.BoundClaims { + boundClaimsElements[key] = types.StringValue(value) + } + + boundClaimsMapValue, diags := types.MapValue(types.StringType, boundClaimsElements) + diagnose.Append(diags...) + if diagnose.HasError() { + return + } + + plan.BoundClaims = boundClaimsMapValue + + plan.BoundAudiences, diags = types.ListValueFrom(ctx, types.StringType, infisicalstrings.StringSplitAndTrim(newIdentityOidcAuth.BoundAudiences, ",")) + diagnose.Append(diags...) + if diagnose.HasError() { + return + } + + planAccessTokenTrustedIps := make([]IdentityOidcAuthResourceTrustedIps, len(newIdentityOidcAuth.AccessTokenTrustedIPS)) + for i, el := range newIdentityOidcAuth.AccessTokenTrustedIPS { + if el.Prefix != nil { + planAccessTokenTrustedIps[i] = IdentityOidcAuthResourceTrustedIps{IpAddress: types.StringValue( + el.IpAddress + "/" + strconv.Itoa(*el.Prefix), + )} + } else { + planAccessTokenTrustedIps[i] = IdentityOidcAuthResourceTrustedIps{IpAddress: types.StringValue( + el.IpAddress, + )} + } + } + + stateAccessTokenTrustedIps, diags := types.ListValueFrom(ctx, types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "ip_address": types.StringType, + }, + }, planAccessTokenTrustedIps) + + diagnose.Append(diags...) + if diagnose.HasError() { + return + } + + plan.AccessTokenTrustedIps = stateAccessTokenTrustedIps +} + +// Create creates the resource and sets the initial Terraform state. +func (r *IdentityOidcAuthResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if !r.client.Config.IsMachineIdentityAuth { + resp.Diagnostics.AddError( + "Unable to create identity oidc auth", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan IdentityOidcAuthResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + accessTokenTrustedIps := tfPlanExpandIpFieldAsApiField(ctx, resp.Diagnostics, plan.AccessTokenTrustedIps) + boundAudiences := terraform.StringListToGoStringSlice(ctx, resp.Diagnostics, plan.BoundAudiences) + + boundClaimsMap := make(map[string]string) + for key, value := range plan.BoundClaims.Elements() { + boundClaimsMap[key] = value.(types.String).ValueString() + } + + newIdentityOidcAuth, err := r.client.CreateIdentityOidcAuth(infisical.CreateIdentityOidcAuthRequest{ + IdentityID: plan.IdentityID.ValueString(), + AccessTokenTTL: plan.AccessTokenTTL.ValueInt64(), + AccessTokenMaxTTL: plan.AccessTokenMaxTTL.ValueInt64(), + AccessTokenNumUsesLimit: plan.AccessTokenNumUsesLimit.ValueInt64(), + AccessTokenTrustedIPS: accessTokenTrustedIps, + OidcDiscoveryUrl: plan.OidcDiscoveryUrl.ValueString(), + BoundAudiences: strings.Join(boundAudiences, ","), + BoundIssuer: plan.BoundIssuer.ValueString(), + BoundSubject: plan.BoundSubject.ValueString(), + BoundClaims: boundClaimsMap, + CACERT: plan.CaCertificate.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error creating identity oidc auth", + "Couldn't save oidc auth to Infiscial, unexpected error: "+err.Error(), + ) + return + } + + plan.ID = types.StringValue(newIdentityOidcAuth.ID) + updateOidcAuthStateByApi(ctx, resp.Diagnostics, &plan, &newIdentityOidcAuth) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *IdentityOidcAuthResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if !r.client.Config.IsMachineIdentityAuth { + resp.Diagnostics.AddError( + "Unable to get identity oidc auth", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Get current state + var state IdentityOidcAuthResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get the latest data from the API + identityOidcAuth, err := r.client.GetIdentityOidcAuth(infisical.GetIdentityOidcAuthRequest{ + IdentityID: state.IdentityID.ValueString(), + }) + + if err != nil { + if err == infisicalclient.ErrNotFound { + resp.State.RemoveResource(ctx) + return + } else { + resp.Diagnostics.AddError( + "Error reading identity oidc auth", + "Couldn't read identity oidc auth from Infiscial, unexpected error: "+err.Error(), + ) + return + } + } + + updateOidcAuthStateByApi(ctx, resp.Diagnostics, &state, &identityOidcAuth) + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *IdentityOidcAuthResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if !r.client.Config.IsMachineIdentityAuth { + resp.Diagnostics.AddError( + "Unable to update identity oidc auth", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + // Retrieve values from plan + var plan IdentityOidcAuthResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var state IdentityOidcAuthResourceModel + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + accessTokenTrustedIps := tfPlanExpandIpFieldAsApiField(ctx, resp.Diagnostics, plan.AccessTokenTrustedIps) + boundAudiences := terraform.StringListToGoStringSlice(ctx, resp.Diagnostics, plan.BoundAudiences) + + boundClaimsMap := make(map[string]string) + for key, value := range plan.BoundClaims.Elements() { + boundClaimsMap[key] = value.(types.String).ValueString() + } + + updatedIdentityOidcAuth, err := r.client.UpdateIdentityOidcAuth(infisical.UpdateIdentityOidcAuthRequest{ + IdentityID: plan.IdentityID.ValueString(), + AccessTokenTTL: plan.AccessTokenTTL.ValueInt64(), + AccessTokenMaxTTL: plan.AccessTokenMaxTTL.ValueInt64(), + AccessTokenNumUsesLimit: plan.AccessTokenNumUsesLimit.ValueInt64(), + AccessTokenTrustedIPS: accessTokenTrustedIps, + OidcDiscoveryUrl: plan.OidcDiscoveryUrl.ValueString(), + BoundAudiences: strings.Join(boundAudiences, ","), + BoundIssuer: plan.BoundIssuer.ValueString(), + BoundSubject: plan.BoundSubject.ValueString(), + BoundClaims: boundClaimsMap, + CACERT: plan.CaCertificate.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error updating identity oidc auth", + "Couldn't update identity oidc auth from Infiscial, unexpected error: "+err.Error(), + ) + return + } + + updateOidcAuthStateByApi(ctx, resp.Diagnostics, &plan, &updatedIdentityOidcAuth) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *IdentityOidcAuthResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + + if !r.client.Config.IsMachineIdentityAuth { + resp.Diagnostics.AddError( + "Unable to delete identity oidc auth", + "Only Machine Identity authentication is supported for this operation", + ) + return + } + + var state IdentityOidcAuthResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.RevokeIdentityOidcAuth(infisical.RevokeIdentityOidcAuthRequest{ + IdentityID: state.IdentityID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting identity oidc auth", + "Couldn't delete identity oidc auth from Infiscial, unexpected error: "+err.Error(), + ) + return + } +} From f8aed846c304a828c111e56c793d1da08c9dd753 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 13 Sep 2024 21:15:15 +0800 Subject: [PATCH 2/3] misc: addressed lint issues --- internal/pkg/strings/main.go | 4 ++-- .../provider/resource/identity_oidc_auth.go | 20 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/pkg/strings/main.go b/internal/pkg/strings/main.go index 64b71ca..e5772d2 100644 --- a/internal/pkg/strings/main.go +++ b/internal/pkg/strings/main.go @@ -2,8 +2,8 @@ package pkg import "strings" -func StringSplitAndTrim(input string, seperator string) []string { - splittedStrings := strings.Split(input, seperator) +func StringSplitAndTrim(input string, separator string) []string { + splittedStrings := strings.Split(input, separator) for i := 0; i < len(splittedStrings); i++ { splittedStrings[i] = strings.TrimSpace(splittedStrings[i]) } diff --git a/internal/provider/resource/identity_oidc_auth.go b/internal/provider/resource/identity_oidc_auth.go index 6f187da..822edb9 100644 --- a/internal/provider/resource/identity_oidc_auth.go +++ b/internal/provider/resource/identity_oidc_auth.go @@ -231,7 +231,15 @@ func (r *IdentityOidcAuthResource) Create(ctx context.Context, req resource.Crea boundClaimsMap := make(map[string]string) for key, value := range plan.BoundClaims.Elements() { - boundClaimsMap[key] = value.(types.String).ValueString() + if strVal, ok := value.(types.String); ok { + boundClaimsMap[key] = strVal.ValueString() + } else { + resp.Diagnostics.AddError( + "Error creating identity oidc auth", + "Bound claims value is not a string", + ) + return + } } newIdentityOidcAuth, err := r.client.CreateIdentityOidcAuth(infisical.CreateIdentityOidcAuthRequest{ @@ -340,7 +348,15 @@ func (r *IdentityOidcAuthResource) Update(ctx context.Context, req resource.Upda boundClaimsMap := make(map[string]string) for key, value := range plan.BoundClaims.Elements() { - boundClaimsMap[key] = value.(types.String).ValueString() + if strVal, ok := value.(types.String); ok { + boundClaimsMap[key] = strVal.ValueString() + } else { + resp.Diagnostics.AddError( + "Error updating identity oidc auth", + "Bound claims value is not a string", + ) + return + } } updatedIdentityOidcAuth, err := r.client.UpdateIdentityOidcAuth(infisical.UpdateIdentityOidcAuthRequest{ From cb8664fbe4a10848cf1b5f04616b0d3276a61fde Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Sun, 15 Sep 2024 18:38:16 +0800 Subject: [PATCH 3/3] misc: addressed unknown after apply issues --- .../provider/resource/identity_oidc_auth.go | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/internal/provider/resource/identity_oidc_auth.go b/internal/provider/resource/identity_oidc_auth.go index 822edb9..df4e220 100644 --- a/internal/provider/resource/identity_oidc_auth.go +++ b/internal/provider/resource/identity_oidc_auth.go @@ -14,6 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" @@ -78,27 +81,31 @@ func (r *IdentityOidcAuthResource) Schema(_ context.Context, _ resource.SchemaRe Required: true, }, "bound_audiences": schema.ListAttribute{ - Description: "The comma-separated list of intended recipients.", - Optional: true, - Computed: true, - ElementType: types.StringType, + Description: "The comma-separated list of intended recipients.", + Optional: true, + Computed: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, }, "bound_claims": schema.MapAttribute{ - Description: "The attributes that should be present in the JWT for it to be valid. The provided values can be a glob pattern.", - Optional: true, - Computed: true, - ElementType: types.StringType, + Description: "The attributes that should be present in the JWT for it to be valid. The provided values can be a glob pattern.", + Optional: true, + Computed: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.Map{mapplanmodifier.UseStateForUnknown()}, }, "bound_subject": schema.StringAttribute{ - Description: "The expected principal that is the subject of the JWT.", - Optional: true, - Computed: true, + Description: "The expected principal that is the subject of the JWT.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "oidc_ca_certificate": schema.StringAttribute{ Description: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints", MarkdownDescription: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints", Optional: true, Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, }, "access_token_trusted_ips": schema.ListNestedAttribute{ Optional: true, @@ -112,21 +119,25 @@ func (r *IdentityOidcAuthResource) Schema(_ context.Context, _ resource.SchemaRe }, }, }, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, }, "access_token_ttl": schema.Int64Attribute{ - Description: "The lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000", - Computed: true, - Optional: true, + Description: "The lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, }, "access_token_max_ttl": schema.Int64Attribute{ - Description: "The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000", - Computed: true, - Optional: true, + Description: "The maximum lifetime for an access token in seconds. This value will be referenced at renewal time. Default: 2592000", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, }, "access_token_num_uses_limit": schema.Int64Attribute{ - Description: "The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. Default:0", - Computed: true, - Optional: true, + Description: "The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses. Default:0", + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, }, }, }