Skip to content

Commit

Permalink
adding api_oidc_config support
Browse files Browse the repository at this point in the history
This change adds a new API OIDC Config resource type.
  • Loading branch information
kpatron-cockroachlabs committed Aug 21, 2023
1 parent c78f4f5 commit 08d8620
Show file tree
Hide file tree
Showing 17 changed files with 1,683 additions and 10 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- New api_oidc_config resource allows users to configure an external JWT signer for API tokens.
JWT API tokens are in [limited access](https://www.cockroachlabs.com/docs/v23.1/cockroachdb-feature-availability).

## [1.1.0] - 2023-08-15

### Added
Expand Down
30 changes: 30 additions & 0 deletions docs/resources/api_oidc_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "cockroach_api_oidc_config Resource - terraform-provider-cockroach"
subcategory: ""
description: |-
Configuration to allow external OIDC providers to issue tokens for use with CC API.
---

# cockroach_api_oidc_config (Resource)

Configuration to allow external OIDC providers to issue tokens for use with CC API.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `audience` (String) The audience that CC API should accept for this API OIDC Configuration.
- `claim` (String) The JWT claim that should be used as the user identifier. Defaults to the subject if an empty string is provided.
- `identity_map` (String) The mapping rules to convert token user identifiers into a new form.
- `issuer` (String) The issuer of tokens for the API OIDC Configuration. Usually this is a url.
- `jwks` (String) The JSON Web Key Set used to check the signature of the JWTs.

### Read-Only

- `id` (String) ID of the API OIDC Configuration. Required by Terraform.


Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "cockroach_api_oidc_config" "example" {
issuer = "https://accounts.google.com"
audience = "test_audience"
jwks = "{\"keys\":[{\"alg\":\"RS256\",\"e\":\"AQAB\",\"kid\":\"test_kid1\",\"kty\":\"RSA\",\"n\":\"09lq1lCEuteonwDJOhGTDak11ThplZuC9JEWQNdBnBSQwlkJQIE7A7nTBO0xTibcsh2HwYkC-N_Gs1jP4iwN3dRqnu5FwG2ct5mY8KLwJiHzToFC0MKenSFQCy0FviNtOnpiObcUlDvR2NDeNtMl_6SPzcQEt7GUTBBYZgoAxPmOgevki6ZNO6Y86xFqx3y6v8EPwW010AiC60r4AHGCTBhYF4uqmq5JH2UU4dDh9Udc-9LZxlSqPwJvnKDG2GjcnD8TsU3wjfEM_nRmx3dnXsrZUXYfNGtdv5dlHywf5AhkJmTavqcsJkgrNA-PNBghFMcCR816_kCIkCYWLWC5vQ\"}]}"
claim = "sub"
identity_map = "token_username cc_username \n /(.*) \\[email protected]"
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/cockroachdb/terraform-provider-cockroach
go 1.18

require (
github.com/cockroachdb/cockroach-cloud-sdk-go v1.4.0
github.com/cockroachdb/cockroach-cloud-sdk-go v1.5.0
github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0
github.com/hashicorp/go-retryablehttp v0.7.4
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.4.0 h1:VLXNL7oYG5ySlPWaUlucHdiFTCCSViVobrC1b9Tg5Mg=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.4.0/go.mod h1:oG9ylbcVGOF7IbVAW2nx5F6ry9a2dZD1H9rd+qd4P60=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.5.0 h1:lrCbcOaUIpehAa4IsZhPtbZwfM/6s9K/GvN24+wDPvM=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.5.0/go.mod h1:oG9ylbcVGOF7IbVAW2nx5F6ry9a2dZD1H9rd+qd4P60=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -274,8 +274,6 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
248 changes: 248 additions & 0 deletions internal/provider/api_oidc_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/*
Copyright 2023 The Cockroach Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package provider

import (
"context"
"fmt"
"github.com/cockroachdb/cockroach-cloud-sdk-go/pkg/client"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"net/http"
)

type apiOidcConfigResource struct {
provider *provider
}

func (r *apiOidcConfigResource) Schema(
_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
MarkdownDescription: "Configuration to allow external OIDC providers to issue tokens for use with CC API.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
MarkdownDescription: "ID of the API OIDC Configuration.",
},
"issuer": schema.StringAttribute{
Required: true,
Description: "The issuer of tokens for the API OIDC Configuration. Usually this is a url.",
},
"audience": schema.StringAttribute{
Required: true,
Description: "The audience that CC API should accept for this API OIDC Configuration.",
},
"jwks": schema.StringAttribute{
Required: true,
Description: "The JSON Web Key Set used to check the signature of the JWTs.",
},
"claim": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "The JWT claim that should be used as the user identifier. Defaults to the subject if an empty string is provided.",
},
"identity_map": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "The mapping rules to convert token user identifiers into a new form.",
},
},
}
}

func (r *apiOidcConfigResource) Metadata(
_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_api_oidc_config"
}

func (r *apiOidcConfigResource) Configure(
_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
var ok bool
if r.provider, ok = req.ProviderData.(*provider); !ok {
resp.Diagnostics.AddError("Internal provider error",
fmt.Sprintf("Error in Configure: expected %T but got %T", provider{}, req.ProviderData))
}
}

func (r *apiOidcConfigResource) Create(
ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse,
) {
if r.provider == nil || !r.provider.configured {
addConfigureProviderErr(&resp.Diagnostics)
return
}

var apiOIdcConfigSpec ApiOidcConfig
diags := req.Plan.Get(ctx, &apiOIdcConfigSpec)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

createRequest := &client.CreateApiOidcConfigRequest{
Audience: apiOIdcConfigSpec.Audience.ValueString(),
Issuer: apiOIdcConfigSpec.Issuer.ValueString(),
Jwks: apiOIdcConfigSpec.Jwks.ValueString(),
Claim: apiOIdcConfigSpec.Claim.ValueStringPointer(),
IdentityMap: apiOIdcConfigSpec.IdentityMap.ValueStringPointer(),
}

apiResp, _, err := r.provider.service.CreateApiOidcConfig(ctx, createRequest)
if err != nil {
resp.Diagnostics.AddError(
"Error creating API OIDC Config",
fmt.Sprintf("Could not create API OIDC Config: %s", formatAPIErrorMessage(err)),
)
return
}

loadApiOidcConfigToTerraformState(apiResp, &apiOIdcConfigSpec)
diags = resp.State.Set(ctx, apiOIdcConfigSpec)
resp.Diagnostics.Append(diags...)
}

func (r *apiOidcConfigResource) Read(
ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse,
) {
if r.provider == nil || !r.provider.configured {
addConfigureProviderErr(&resp.Diagnostics)
return
}

var state ApiOidcConfig
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

apiResp, httpResp, err := r.provider.service.GetApiOidcConfig(ctx, state.ID.ValueString())
if err != nil {
if httpResp != nil && httpResp.StatusCode == http.StatusNotFound {
resp.Diagnostics.AddWarning(
"API OIDC Config not found",
"API OIDC Config not found. API OIDC Config will be removed from state.")
resp.State.RemoveResource(ctx)
} else {
resp.Diagnostics.AddError(
"Error getting API OIDC Config",
fmt.Sprintf("Unexpected error retrieving API OIDC Config: %s", formatAPIErrorMessage(err)))
}
return
}

loadApiOidcConfigToTerraformState(apiResp, &state)

diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}

func (r *apiOidcConfigResource) Update(
ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse,
) {
var plan ApiOidcConfig
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// Get current state
var state ApiOidcConfig
diags = req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

apiResp, _, err := r.provider.service.UpdateApiOidcConfig(ctx, plan.ID.ValueString(), &client.ApiOidcConfig1{
Audience: plan.Audience.ValueString(),
Claim: plan.Claim.ValueStringPointer(),
IdentityMap: plan.IdentityMap.ValueStringPointer(),
Issuer: plan.Issuer.ValueString(),
Jwks: plan.Jwks.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
"Error update API OIDC Config",
fmt.Sprintf("Could not update API OIDC Config: %s", formatAPIErrorMessage(err)),
)
return
}

loadApiOidcConfigToTerraformState(apiResp, &state)
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}

func (r *apiOidcConfigResource) Delete(
ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse,
) {
var state ApiOidcConfig
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

_, _, err := r.provider.service.DeleteApiOidcConfig(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error deleting API OIDC Config",
fmt.Sprintf("Could not delete API OIDC Config: %s", formatAPIErrorMessage(err)),
)
return
}

// Remove resource from state
resp.State.RemoveResource(ctx)
}

func (r *apiOidcConfigResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

func NewApiOidcConfigResource() resource.Resource {
return &apiOidcConfigResource{}
}

func loadApiOidcConfigToTerraformState(
apiOidcConfig *client.ApiOidcConfig, state *ApiOidcConfig,
) {
state.ID = types.StringValue(apiOidcConfig.Id)
state.Audience = types.StringValue(apiOidcConfig.Audience)
state.Issuer = types.StringValue(apiOidcConfig.Issuer)
state.Jwks = types.StringValue(apiOidcConfig.Jwks)
state.Claim = types.StringPointerValue(apiOidcConfig.Claim)
state.IdentityMap = types.StringPointerValue(apiOidcConfig.IdentityMap)
}
Loading

0 comments on commit 08d8620

Please sign in to comment.