Skip to content

Commit

Permalink
Add Organizations for Client Credentials (auth0#1009)
Browse files Browse the repository at this point in the history
* Updated schema and added relevant test cases for client with default_organization

* Added test recordings

* Updated the resource schema and added new test recordinging

* Added validation on required arguments

* Fixed linting and updated test recording

* Deleted a test recording

* Updated go.mod

Signed-off-by: BryanLewis-AtOkta <[email protected]>
  • Loading branch information
duedares-rvj authored and bryanlewis-okta committed Oct 24, 2024
1 parent 9943147 commit e510efd
Show file tree
Hide file tree
Showing 8 changed files with 2,240 additions and 584 deletions.
11 changes: 11 additions & 0 deletions docs/data-sources/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ data "auth0_client" "some-client-by-id" {
- `cross_origin_loc` (String) URL of the location in your site where the cross-origin verification takes place for the cross-origin auth flow when performing authentication in your own domain instead of Auth0 Universal Login page.
- `custom_login_page` (String) The content (HTML, CSS, JS) of the custom login page.
- `custom_login_page_on` (Boolean) Indicates whether a custom login page is to be used.
- `default_organization` (List of Object) Configure and associate an organization with the Client (see [below for nested schema](#nestedatt--default_organization))
- `description` (String) Description of the purpose of the client.
- `encryption_key` (Map of String) Encryption used for WS-Fed responses with this client.
- `form_template` (String) HTML form template to be used for WS-Federation.
Expand Down Expand Up @@ -402,6 +403,16 @@ Read-Only:



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

Read-Only:

- `disable` (Boolean)
- `flows` (List of String)
- `organization_id` (String)


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

Expand Down
11 changes: 11 additions & 0 deletions docs/resources/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ resource "auth0_client" "my_client" {
- `cross_origin_loc` (String) URL of the location in your site where the cross-origin verification takes place for the cross-origin auth flow when performing authentication in your own domain instead of Auth0 Universal Login page.
- `custom_login_page` (String) The content (HTML, CSS, JS) of the custom login page.
- `custom_login_page_on` (Boolean) Indicates whether a custom login page is to be used.
- `default_organization` (Block List, Max: 1) Configure and associate an organization with the Client (see [below for nested schema](#nestedblock--default_organization))
- `description` (String) Description of the purpose of the client.
- `encryption_key` (Map of String) Encryption used for WS-Fed responses with this client.
- `form_template` (String) HTML form template to be used for WS-Federation.
Expand Down Expand Up @@ -448,6 +449,16 @@ Optional:



<a id="nestedblock--default_organization"></a>
### Nested Schema for `default_organization`

Optional:

- `disable` (Boolean) If set, the `default_organization` will be removed.
- `flows` (List of String) Definition of the flow that needs to be configured. Eg. client_credentials
- `organization_id` (String) The unique identifier of the organization


<a id="nestedblock--jwt_configuration"></a>
### Nested Schema for `jwt_configuration`

Expand Down
50 changes: 48 additions & 2 deletions internal/auth0/client/expand.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package client

import (
"encoding/json"

"github.com/auth0/go-auth0"
"github.com/auth0/go-auth0/management"
"github.com/hashicorp/go-cty/cty"
Expand All @@ -9,7 +11,12 @@ import (
"github.com/auth0/terraform-provider-auth0/internal/value"
)

func expandClient(data *schema.ResourceData) *management.Client {
type nilableClient struct {
management.Client
DefaultOrganization *management.ClientDefaultOrganization `json:"default_organization"`
}

func expandClient(data *schema.ResourceData) (interface{}, error) {
config := data.GetRawConfig()

client := &management.Client{
Expand Down Expand Up @@ -61,7 +68,46 @@ func expandClient(data *schema.ResourceData) *management.Client {
}
}

return client
defaultOrg := config.GetAttr("default_organization")

if !defaultOrg.IsNull() && defaultOrg.LengthInt() > 0 {
if defaultOrg.AsValueSlice()[0].GetAttr("disable").True() {
clientJSON, err := json.Marshal(client)
if err != nil {
return nil, err
}

nilableClient := nilableClient{}
if err := json.Unmarshal(clientJSON, &nilableClient); err != nil {
return nil, err
}
nilableClient.DefaultOrganization = nil
return nilableClient, nil
}
client.DefaultOrganization = expandDefaultOrganization(data)
}

return client, nil
}

func expandDefaultOrganization(data *schema.ResourceData) *management.ClientDefaultOrganization {
var defaultOrg management.ClientDefaultOrganization

defaultOrganizationConfig := data.GetRawConfig().GetAttr("default_organization")
if defaultOrganizationConfig.IsNull() {
return nil
}

defaultOrganizationConfig.ForEachElement(func(_ cty.Value, config cty.Value) (stop bool) {
defaultOrg.Flows = value.Strings(config.GetAttr("flows"))
defaultOrg.OrganizationID = value.String(config.GetAttr("organization_id"))
return stop
})
if defaultOrg == (management.ClientDefaultOrganization{}) {
return nil
}

return &defaultOrg
}

func expandOIDCBackchannelLogout(data *schema.ResourceData) *management.OIDCBackchannelLogout {
Expand Down
14 changes: 14 additions & 0 deletions internal/auth0/client/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,19 @@ func flattenClientAddonSAML2(addon *management.SAML2ClientAddon) []interface{} {
}
}

func flattenDefaultOrganization(defaultOrganization *management.ClientDefaultOrganization) []interface{} {
do := make(map[string]interface{})

if defaultOrganization == nil {
do["disable"] = true
} else {
do["flows"] = defaultOrganization.GetFlows()
do["organization_id"] = defaultOrganization.GetOrganizationID()
}

return []interface{}{do}
}

func flattenClient(data *schema.ResourceData, client *management.Client) error {
result := multierror.Append(
data.Set("client_id", client.GetClientID()),
Expand Down Expand Up @@ -543,6 +556,7 @@ func flattenClient(data *schema.ResourceData, client *management.Client) error {
data.Set("client_metadata", client.GetClientMetadata()),
data.Set("oidc_backchannel_logout_urls", client.GetOIDCBackchannelLogout().GetBackChannelLogoutURLs()),
data.Set("require_pushed_authorization_requests", client.GetRequirePushedAuthorizationRequests()),
data.Set("default_organization", flattenDefaultOrganization(client.GetDefaultOrganization())),
)
return result.ErrorOrNil()
}
Expand Down
79 changes: 72 additions & 7 deletions internal/auth0/client/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package client

import (
"context"
"encoding/json"
"net/http"

"github.com/auth0/go-auth0/management"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand Down Expand Up @@ -1271,20 +1273,66 @@ func NewResource() *schema.Resource {
},
},
},
"default_organization": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Configure and associate an organization with the Client",
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"flows": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
RequiredWith: []string{"default_organization.0.organization_id"},
ConflictsWith: []string{"default_organization.0.disable"},
Description: "Definition of the flow that needs to be configured. Eg. client_credentials",
},
"organization_id": {
Type: schema.TypeString,
Optional: true,
RequiredWith: []string{"default_organization.0.flows"},
ConflictsWith: []string{"default_organization.0.disable"},
Description: "The unique identifier of the organization",
},
"disable": {
Type: schema.TypeBool,
Optional: true,
ConflictsWith: []string{"default_organization.0.organization_id", "default_organization.0.flows"},
Description: "If set, the `default_organization` will be removed.",
},
},
},
},
},
}
}

func createClient(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()
client, err := expandClient(data)

client := expandClient(data)
if err != nil {
return diag.FromErr(err)
}
err = api.Request(ctx, http.MethodPost, api.URI("clients"), client)
if err != nil {
return diag.FromErr(err)
}

if err := api.Client.Create(ctx, client); err != nil {
baseClient := management.Client{}
clientJSON, err := json.Marshal(client)

if err != nil {
return diag.FromErr(err)
}

if err = json.Unmarshal(clientJSON, &baseClient); err != nil {
return diag.FromErr(err)
}

data.SetId(client.GetClientID())
data.SetId(baseClient.GetClientID())

return readClient(ctx, data, meta)
}
Expand All @@ -1304,8 +1352,24 @@ func readClient(ctx context.Context, data *schema.ResourceData, meta interface{}
func updateClient(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
api := meta.(*config.Config).GetAPI()

if client := expandClient(data); clientHasChange(client) {
if client.GetAddons() != nil {
client, err := expandClient(data)
if err != nil {
return diag.FromErr(err)
}

baseClient := management.Client{}
clientJSON, err := json.Marshal(client)

if err != nil {
return diag.FromErr(err)
}

if err = json.Unmarshal(clientJSON, &baseClient); err != nil {
return diag.FromErr(err)
}

if clientHasChange(&baseClient) {
if baseClient.GetAddons() != nil {
// In case we are switching addons, we need to be able to clear out the previous config.
resetAddons := &management.Client{
Addons: &management.ClientAddons{},
Expand All @@ -1315,8 +1379,9 @@ func updateClient(ctx context.Context, data *schema.ResourceData, meta interface
}
}

if err := api.Client.Update(ctx, data.Id(), client); err != nil {
return diag.FromErr(internalError.HandleAPIError(data, err))
err = api.Request(ctx, http.MethodPatch, api.URI("clients", data.Id()), client)
if err != nil {
return diag.FromErr(err)
}
}

Expand Down
112 changes: 112 additions & 0 deletions internal/auth0/client/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2315,3 +2315,115 @@ func TestAccClientCanSetDefaultAuthMethodOnCreate(t *testing.T) {
},
})
}

const testAccCreateClientWithDefaultOrganization = `
resource "auth0_organization" "my_org" {
name = "temp-org"
display_name = "temp-org"
}
data "auth0_organization" "my_org" {
depends_on = [ resource.auth0_organization.my_org ]
name = "temp-org"
}
resource "auth0_client" "my_client" {
depends_on = [ data.auth0_organization.my_org ]
name = "Acceptance Test - DefaultOrganization - {{.testName}}"
default_organization {
flows = ["client_credentials"]
organization_id = data.auth0_organization.my_org.id
}
}
`

const testAccUpdateClientWithDefaultOrganization = `
resource "auth0_organization" "my_new_org" {
name = "temp-new-org"
display_name = "temp-new-org"
}
data "auth0_organization" "my_new_org" {
depends_on = [ resource.auth0_organization.my_new_org ]
name = "temp-new-org"
}
resource "auth0_client" "my_client" {
depends_on = [ data.auth0_organization.my_new_org ]
name = "Acceptance Test - DefaultOrganization - {{.testName}}"
default_organization {
flows = ["client_credentials"]
organization_id = data.auth0_organization.my_new_org.id
}
}
`

const testAccUpdateClientRemoveDefaultOrganization = `
resource "auth0_client" "my_client" {
name = "Acceptance Test - DefaultOrganization - {{.testName}}"
default_organization {
disable = true
}
}
`

const testAccUpdateClientDefaultOrganizationFlowsOnly = `
resource "auth0_client" "my_client" {
name = "Acceptance Test - DefaultOrganization - {{.testName}}"
default_organization {
flows = ["client_credentials"]
}
}
`

const testAccUpdateClientDefaultOrganizationOrgIDOnly = `
resource "auth0_client" "my_client" {
name = "Acceptance Test - DefaultOrganization - {{.testName}}"
default_organization {
organization_id = "org_z5YvxlXPO0NspoIa"
}
}
`

func TestAccClientWithDefaultOrganization(t *testing.T) {
acctest.Test(t, resource.TestCase{
Steps: []resource.TestStep{
{
Config: acctest.ParseTestName(testAccUpdateClientDefaultOrganizationFlowsOnly, t.Name()),
ExpectError: regexp.MustCompile("Error: Missing required argument"),
},
{
Config: acctest.ParseTestName(testAccUpdateClientDefaultOrganizationOrgIDOnly, t.Name()),
ExpectError: regexp.MustCompile("Error: Missing required argument"),
},
{
Config: acctest.ParseTestName(testAccCreateClientWithDefaultOrganization, t.Name()),
Check: resource.ComposeTestCheckFunc(

resource.TestCheckResourceAttr("auth0_client.my_client", "name", fmt.Sprintf("Acceptance Test - DefaultOrganization - %s", t.Name())),
resource.TestCheckResourceAttr("auth0_client.my_client", "default_organization.0.flows.0", "client_credentials"),
resource.TestCheckResourceAttrSet("auth0_client.my_client", "default_organization.0.organization_id"),
),
},
{
Config: acctest.ParseTestName(testAccUpdateClientWithDefaultOrganization, t.Name()),
Check: resource.ComposeTestCheckFunc(

resource.TestCheckResourceAttr("auth0_client.my_client", "name", fmt.Sprintf("Acceptance Test - DefaultOrganization - %s", t.Name())),
resource.TestCheckResourceAttr("auth0_client.my_client", "default_organization.0.flows.0", "client_credentials"),
resource.TestCheckResourceAttrSet("auth0_client.my_client", "default_organization.0.organization_id"),
),
},
{
Config: acctest.ParseTestName(testAccUpdateClientRemoveDefaultOrganization, t.Name()),
Check: resource.ComposeTestCheckFunc(

resource.TestCheckResourceAttr("auth0_client.my_client", "name", fmt.Sprintf("Acceptance Test - DefaultOrganization - %s", t.Name())),
resource.TestCheckResourceAttr("auth0_client.my_client", "default_organization.0.disable", "true"),
resource.TestCheckResourceAttr("auth0_client.my_client", "default_organization.0.flows.#", "0"),
resource.TestCheckResourceAttr("auth0_client.my_client", "default_organization.0.organization_id", ""),
),
},
},
})
}
Loading

0 comments on commit e510efd

Please sign in to comment.