Skip to content

Commit

Permalink
Merge pull request #2241 from okta/issue_2190_okta_inline_hook_channe…
Browse files Browse the repository at this point in the history
…l_json

Full channel API contract as JSON in `okta_inline_hook` resource
  • Loading branch information
monde authored Mar 7, 2025
2 parents 182fc7e + 83743bb commit a91877c
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 17 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 4.15.0 (Mar 06, 2025)

### IMPROVEMENTS

* Add `channel_json` argument to `okta_inline_hook` resource allowing direct configuration of a hook [#2241](https://github.com/okta/terraform-provider-okta/pull/2241). Thanks, [@monde](https://github.com/monde)!


## 4.14.1 (Mar 03, 2025)

### BUG FIXES
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ terraform {
required_providers {
okta = {
source = "okta/okta"
version = "~> 4.14.1"
version = "~> 4.15.0"
}
}
}
Expand Down
48 changes: 46 additions & 2 deletions docs/resources/inline_hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Creates an inline hook. This resource allows you to create and configure an inli

## Example Usage

### HTTP Auth
```terraform
resource "okta_inline_hook" "example" {
name = "example"
Expand All @@ -30,19 +31,62 @@ resource "okta_inline_hook" "example" {
}
```

### OAuth2.0 Auth
```terraform
resource "okta_inline_hook" "example" {
name = "example"
version = "1.0.0"
type = "com.okta.saml.tokens.transform"
status = "ACTIVE"
channel_json = <<JSON
{
"type": "OAUTH",
"version": "1.0.0",
"config": {
"headers": [
{
"key": "Field 1",
"value": "Value 1"
},
{
"key": "Field 2",
"value": "Value 2"
}
],
"method": "POST",
"authType": "client_secret_post",
"uri": "https://example.com/service",
"clientId": "abc123",
"clientSecret": "fake-secret",
"tokenUrl": "https://example.com/token",
"scope": "api"
}
}
JSON
}
```

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

### Required

- `channel` (Map of String)
- `name` (String) The inline hook display name.
- `type` (String) The type of hook to create. [See here for supported types](https://developer.okta.com/docs/reference/api/inline-hooks/#supported-inline-hook-types).
- `version` (String) The version of the hook. The currently-supported version is `1.0.0`.

### Optional

- `auth` (Map of String)
-> The original implementation of `okta_inline_hook` did not correctly expose
all of the required channel arguments needed for OAuth2.0 Authentication. Make
use of `channel_json` for more expressive channel value arguments for the inline
hook.

- `channel` (Map of String, excludes channel_json)
- `auth` (Map of String, excludes channel_json)
- `channel_json` (JSON String, excludes channel and auth) true channel object for the inline hook API contract
- `headers` (Block Set) Map of headers to send along in inline hook request. (see [below for nested schema](#nestedblock--headers))
- `status` (String) Default to `ACTIVE`

Expand Down
2 changes: 1 addition & 1 deletion okta/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
)

const (
OktaTerraformProviderVersion = "4.14.1"
OktaTerraformProviderVersion = "4.15.0"
OktaTerraformProviderUserAgent = "okta-terraform/" + OktaTerraformProviderVersion
)

Expand Down
91 changes: 83 additions & 8 deletions okta/resource_okta_inline_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package okta

import (
"context"
"encoding/json"
"reflect"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -57,6 +59,7 @@ func resourceInlineHook() *schema.Resource {
Elem: headerSchema,
Description: "Map of headers to send along in inline hook request.",
},
// channel and auth presumed to work together
"auth": {
Type: schema.TypeMap,
Optional: true,
Expand All @@ -69,10 +72,11 @@ func resourceInlineHook() *schema.Resource {
}
return false
},
ConflictsWith: []string{"channel_json"},
},
"channel": {
Type: schema.TypeMap,
Required: true,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Expand All @@ -85,6 +89,16 @@ func resourceInlineHook() *schema.Resource {
}
return false
},
ConflictsWith: []string{"channel_json"},
},
"channel_json": {
Type: schema.TypeString,
Optional: true,
Description: "true channel object for the inline hook API contract",
ValidateDiagFunc: stringIsJSON,
StateFunc: normalizeDataJSON,
DiffSuppressFunc: noChangeInObjectFromUnmarshaledChannelJSON,
ConflictsWith: []string{"channel", "auth"},
},
},
}
Expand Down Expand Up @@ -117,11 +131,31 @@ func resourceInlineHookRead(ctx context.Context, d *schema.ResourceData, meta in
_ = d.Set("status", hook.Status)
_ = d.Set("type", hook.Type)
_ = d.Set("version", hook.Version)
err = setNonPrimitives(d, map[string]interface{}{
"channel": flattenInlineHookChannel(hook.Channel),
"headers": flattenInlineHookHeaders(hook.Channel),
"auth": flattenInlineHookAuth(d, hook.Channel),
})

if oldChannelJson, ok := d.GetOk("channel_json"); ok {
// NOTE: Okta responses don't include config.clientSecret so copy the
// secret over if it exists the existing channel json
var oldChannel sdk.InlineHookChannel
if err = json.Unmarshal([]byte(oldChannelJson.(string)), &oldChannel); err == nil {
if oldChannel.Config != nil && oldChannel.Config.ClientSecret != "" {
if hook.Channel != nil && hook.Channel.Config != nil {
hook.Channel.Config.ClientSecret = oldChannel.Config.ClientSecret
}
}
}

channelJson, err := json.Marshal(hook.Channel)
if err != nil {
return diag.Errorf("error marshaling channel json: %v", err)
}
_ = d.Set("channel_json", string(channelJson))
} else {
err = setNonPrimitives(d, map[string]interface{}{
"channel": flattenInlineHookChannel(hook.Channel),
"headers": flattenInlineHookHeaders(hook.Channel),
"auth": flattenInlineHookAuth(d, hook.Channel),
})
}
if err != nil {
return diag.Errorf("failed to set inline hook properties: %v", err)
}
Expand Down Expand Up @@ -156,13 +190,20 @@ func resourceInlineHookDelete(ctx context.Context, d *schema.ResourceData, meta
}

func buildInlineHook(d *schema.ResourceData) sdk.InlineHook {
return sdk.InlineHook{
inlineHook := sdk.InlineHook{
Name: d.Get("name").(string),
Status: d.Get("status").(string),
Type: d.Get("type").(string),
Version: d.Get("version").(string),
Channel: buildInlineChannel(d),
}
if channelJson, ok := d.GetOk("channel_json"); ok {
var channel sdk.InlineHookChannel
_ = json.Unmarshal([]byte(channelJson.(string)), &channel)
inlineHook.Channel = &channel
} else {
inlineHook.Channel = buildInlineChannel(d)
}
return inlineHook
}

func buildInlineChannel(d *schema.ResourceData) *sdk.InlineHookChannel {
Expand Down Expand Up @@ -260,3 +301,37 @@ func setInlineHookStatus(ctx context.Context, d *schema.ResourceData, client *sd
}
return err
}

// noChangeInObjectFromUnmarshaledChannelJSON is a DiffSuppressFunc returns and
// true if old and new JSONs are equivalent object representations ... It is
// true, there is no change! Edge chase if newJSON is blank, will also return
// true which cover the new resource case. Okta does not return
// config.clientSecret in the response so ignore that value.
func noChangeInObjectFromUnmarshaledChannelJSON(k, oldJSON, newJSON string, d *schema.ResourceData) bool {
if newJSON == "" {
return true
}
var oldObj map[string]any
var newObj map[string]any
if err := json.Unmarshal([]byte(oldJSON), &oldObj); err != nil {
return false
}
if err := json.Unmarshal([]byte(newJSON), &newObj); err != nil {
return false
}

configField := "config"
clientSecretField := "clientSecret"
if config, ok := oldObj[configField]; ok {
if _config, ok := config.(map[string]any); ok {
delete(_config, clientSecretField)
}
}
if config, ok := newObj[configField]; ok {
if _config, ok := config.(map[string]any); ok {
delete(_config, clientSecretField)
}
}

return reflect.DeepEqual(oldObj, newObj)
}
77 changes: 77 additions & 0 deletions okta/resource_okta_inline_hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package okta

import (
"context"
"fmt"
"net/http"
"testing"

Expand Down Expand Up @@ -101,6 +102,82 @@ func TestAccResourceOktaInlineHook_crud(t *testing.T) {
})
}

func TestAccResourceOktaInlineHook_com_okta_saml_tokens_transform(t *testing.T) {
resourceName := "okta_inline_hook.test"
mgr := newFixtureManager("resources", inlineHook, t.Name())

name1 := "One"
name2 := "Two"
config := `
resource "okta_inline_hook" "test" {
name = "testAcc_replace_with_uuid_%s"
type = "com.okta.saml.tokens.transform"
version = "1.0.2"
status = "ACTIVE"
channel_json = <<JSON
{
"type": "OAUTH",
"version": "1.0.0",
"config": {
"headers": [
{
"key": "Field 1",
"value": "Value 1"
},
{
"key": "Field 2",
"value": "Value 2"
}
],
"method": "POST",
"authType": "client_secret_post",
"uri": "https://example.com/service",
"clientId": "abc123",
"clientSecret": "def456",
"tokenUrl": "https://example.com/token",
"scope": "api"
}
}
JSON
}
`

oktaResourceTest(t, resource.TestCase{
PreCheck: testAccPreCheck(t),
ErrorCheck: testAccErrorChecks(t),
ProviderFactories: testAccProvidersFactories,
CheckDestroy: checkResourceDestroy(inlineHook, inlineHookExists),
Steps: []resource.TestStep{
{
Config: mgr.ConfigReplace(fmt.Sprintf(config, name1)),
Check: resource.ComposeTestCheckFunc(
ensureResourceExists(resourceName, inlineHookExists),
resource.TestCheckResourceAttr(resourceName, "name", buildResourceName(mgr.Seed)+"_One"),
resource.TestCheckResourceAttr(resourceName, "status", statusActive),
resource.TestCheckResourceAttr(resourceName, "type", "com.okta.saml.tokens.transform"),
resource.TestCheckResourceAttr(resourceName, "version", "1.0.2"),
resource.TestCheckResourceAttrSet(resourceName, "channel_json"),
resource.TestCheckNoResourceAttr(resourceName, "channel"),
resource.TestCheckNoResourceAttr(resourceName, "auth"),
),
},
{
Config: mgr.ConfigReplace(fmt.Sprintf(config, name2)),
Check: resource.ComposeTestCheckFunc(
ensureResourceExists(resourceName, inlineHookExists),
resource.TestCheckResourceAttr(resourceName, "name", buildResourceName(mgr.Seed)+"_Two"),
resource.TestCheckResourceAttr(resourceName, "status", statusActive),
resource.TestCheckResourceAttr(resourceName, "type", "com.okta.saml.tokens.transform"),
resource.TestCheckResourceAttr(resourceName, "version", "1.0.2"),
resource.TestCheckResourceAttrSet(resourceName, "channel_json"),
resource.TestCheckNoResourceAttr(resourceName, "channel"),
resource.TestCheckNoResourceAttr(resourceName, "auth"),
),
},
},
})
}

func inlineHookExists(id string) (bool, error) {
client := sdkV2ClientForTest()
_, resp, err := client.InlineHook.GetInlineHook(context.Background(), id)
Expand Down
13 changes: 9 additions & 4 deletions sdk/v2_inlineHookChannelConfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
package sdk

type InlineHookChannelConfig struct {
AuthScheme *InlineHookChannelConfigAuthScheme `json:"authScheme,omitempty"`
Headers []*InlineHookChannelConfigHeaders `json:"headers,omitempty"`
Method string `json:"method,omitempty"`
Uri string `json:"uri,omitempty"`
AuthScheme *InlineHookChannelConfigAuthScheme `json:"authScheme,omitempty"`
Headers []*InlineHookChannelConfigHeaders `json:"headers,omitempty"`
Method string `json:"method,omitempty"`
Uri string `json:"uri,omitempty"`
AuthType string `json:"authType,omitempty"`
ClientId string `json:"clientId,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
TokenUrl string `json:"tokenUrl,omitempty"`
Scope string `json:"scope,omitempty"`
}
2 changes: 1 addition & 1 deletion templates/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ terraform {
required_providers {
okta = {
source = "okta/okta"
version = "~> 4.14.1"
version = "~> 4.15.0"
}
}
}
Expand Down

0 comments on commit a91877c

Please sign in to comment.