Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding api_oidc_config support #153

Merged
merged 1 commit into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).

### Changed

- Cluster regions, CMEK regions, log export groups and channels, and private endpoint services and
Expand Down
45 changes: 45 additions & 0 deletions docs/resources/api_oidc_config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
# 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.
- `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.

### Optional

- `claim` (String) The JWT claim that should be used as the user identifier. Defaults to the subject.
- `identity_map` (Attributes List) The mapping rules to convert token user identifiers into a new form. (see [below for nested schema](#nestedatt--identity_map))

### Read-Only

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

<a id="nestedatt--identity_map"></a>
### Nested Schema for `identity_map`

Required:

- `cc_identity` (String) The username (email or service account id) of the CC user that the token should map to.
- `token_identity` (String) The token value that needs to be mapped.

Optional:

- `is_regex` (Boolean) Indicates that the token_principal field is a regex value.


Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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_identity = "token_identity"
cc_identity = "cc_identity"
is_regex = false
},
{
token_identity = "(.*)"
cc_identity = "\\[email protected]"
is_regex = true
},
]
}
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.6.0
github.com/cockroachdb/cockroach-cloud-sdk-go v1.7.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
4 changes: 2 additions & 2 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.6.0 h1:pMBhP9/oodbtP/+h/CCN6mbv58ZZFmAuUNEVIC0zjAA=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.6.0/go.mod h1:oG9ylbcVGOF7IbVAW2nx5F6ry9a2dZD1H9rd+qd4P60=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.7.0 h1:KKokmhXa5nzHXrP8kc81BZ6ciOewcT8FKqofS2yoR1s=
github.com/cockroachdb/cockroach-cloud-sdk-go v1.7.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
295 changes: 295 additions & 0 deletions internal/provider/api_oidc_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
/*
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.",
},
"identity_map": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"token_identity": schema.StringAttribute{
Required: true,
Description: "The token value that needs to be mapped.",
},
"cc_identity": schema.StringAttribute{
Required: true,
Description: "The username (email or service account id) of the CC user that the token should map to.",
},
"is_regex": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "Indicates that the token_principal field is a regex value.",
},
},
},
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: identityMapFromTerraformState(apiOIdcConfigSpec.IdentityMap),
}

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: identityMapFromTerraformState(plan.IdentityMap),
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 = identityMapToTerraformState(apiOidcConfig.IdentityMap)
}

func identityMapFromTerraformState(identityMap *[]IdentityMapEntry) *[]client.ApiOidcIdentityMapEntry {
if identityMap == nil {
return nil
}
var out []client.ApiOidcIdentityMapEntry
for _, mapEntry := range *identityMap {
out = append(out, client.ApiOidcIdentityMapEntry{
CcIdentity: mapEntry.CcIdentity.ValueStringPointer(),
IsRegex: mapEntry.IsRegex.ValueBoolPointer(),
TokenIdentity: mapEntry.TokenIdentity.ValueStringPointer(),
})
}
return &out
}

func identityMapToTerraformState(identityMap *[]client.ApiOidcIdentityMapEntry) *[]IdentityMapEntry {
if identityMap == nil {
return nil
}
var out []IdentityMapEntry
for _, mapEntry := range *identityMap {
out = append(out, IdentityMapEntry{
CcIdentity: types.StringPointerValue(mapEntry.CcIdentity),
IsRegex: types.BoolPointerValue(mapEntry.IsRegex),
TokenIdentity: types.StringPointerValue(mapEntry.TokenIdentity),
})
}
return &out
}
Loading