Skip to content

Commit

Permalink
Add "auth0_clients" data source for listing multiple clients with fil…
Browse files Browse the repository at this point in the history
…tering (#1080)

* Add "auth0_clients" data source for listing multiple clients with filtering

* Add examples of clients data source

* Fix lint errors

* Move clients data source files into client package

* Condense acc tests into one test

---------

Co-authored-by: Rajat Bajaj <rajat.bajaj@okta.com>
  • Loading branch information
devin-brenton and duedares-rvj authored Nov 26, 2024
1 parent 900c692 commit e158550
Show file tree
Hide file tree
Showing 9 changed files with 1,448 additions and 13 deletions.
64 changes: 64 additions & 0 deletions docs/data-sources/clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
page_title: "Data Source: auth0_clients"
description: |-
Data source to retrieve a list of Auth0 application clients with optional filtering.
---

# Data Source: auth0_clients

Data source to retrieve a list of Auth0 application clients with optional filtering.

## Example Usage

```terraform
# Auth0 clients with "External" in the name
data "auth0_clients" "external_apps" {
name_filter = "External"
}
# Auth0 clients filtered by non_interactive or spa app type
data "auth0_clients" "m2m_apps" {
app_types = ["non_interactive", "spa"]
}
# Auth0 clients filtered by is_first_party equal to true
data "auth0_clients" "first_party_apps" {
is_first_party = true
}
```

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

### Optional

- `app_types` (Set of String) Filter clients by application types.
- `is_first_party` (Boolean) Filter clients by first party status.
- `name_filter` (String) Filter clients by name (partial matches supported).

### Read-Only

- `clients` (List of Object) List of clients matching the filter criteria. (see [below for nested schema](#nestedatt--clients))
- `id` (String) The ID of this resource.

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

Read-Only:

- `allowed_clients` (List of String)
- `allowed_logout_urls` (List of String)
- `allowed_origins` (List of String)
- `app_type` (String)
- `callbacks` (List of String)
- `client_id` (String)
- `client_metadata` (Map of String)
- `client_secret` (String)
- `description` (String)
- `grant_types` (List of String)
- `is_first_party` (Boolean)
- `is_token_endpoint_ip_header_trusted` (Boolean)
- `name` (String)
- `web_origins` (List of String)


14 changes: 14 additions & 0 deletions examples/data-sources/auth0_clients/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Auth0 clients with "External" in the name
data "auth0_clients" "external_apps" {
name_filter = "External"
}

# Auth0 clients filtered by non_interactive or spa app type
data "auth0_clients" "m2m_apps" {
app_types = ["non_interactive", "spa"]
}

# Auth0 clients filtered by is_first_party equal to true
data "auth0_clients" "first_party_apps" {
is_first_party = true
}
39 changes: 34 additions & 5 deletions internal/acctest/http_recorder.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,27 +108,56 @@ func redactDomain(i *cassette.Interaction, domain string) {
}

func redactSensitiveDataInClient(t *testing.T, i *cassette.Interaction, domain string) {
create := i.Request.URL == "https://"+domain+"/api/v2/clients" &&
baseURL := "https://" + domain + "/api/v2/clients"
urlPath := strings.Split(i.Request.URL, "?")[0] // Strip query params.

create := i.Request.URL == baseURL &&
i.Request.Method == http.MethodPost

read := strings.Contains(i.Request.URL, "https://"+domain+"/api/v2/clients/") &&
readList := urlPath == baseURL &&
i.Request.Method == http.MethodGet

readOne := strings.Contains(i.Request.URL, baseURL+"/") &&
!strings.Contains(i.Request.URL, "credentials") &&
i.Request.Method == http.MethodGet

update := strings.Contains(i.Request.URL, "https://"+domain+"/api/v2/clients/") &&
update := strings.Contains(i.Request.URL, baseURL+"/") &&
!strings.Contains(i.Request.URL, "credentials") &&
i.Request.Method == http.MethodPatch

if create || read || update {
if create || readList || readOne || update {
if i.Response.Code == http.StatusNotFound {
return
}

redacted := "[REDACTED]"

// Handle list response.
if readList {
var response management.ClientList
err := json.Unmarshal([]byte(i.Response.Body), &response)
require.NoError(t, err)

for _, client := range response.Clients {
client.SigningKeys = []map[string]string{
{"cert": redacted},
}
if client.GetClientSecret() != "" {
client.ClientSecret = &redacted
}
}

responseBody, err := json.Marshal(response)
require.NoError(t, err)
i.Response.Body = string(responseBody)
return
}

// Handle single client response.
var client management.Client
err := json.Unmarshal([]byte(i.Response.Body), &client)
require.NoError(t, err)

redacted := "[REDACTED]"
client.SigningKeys = []map[string]string{
{"cert": redacted},
}
Expand Down
160 changes: 160 additions & 0 deletions internal/auth0/client/data_source_clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package client

import (
"context"
"crypto/sha256"
"fmt"
"strings"

"github.com/auth0/go-auth0/management"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

"github.com/auth0/terraform-provider-auth0/internal/config"
)

// NewClientsDataSource will return a new auth0_clients data source.
func NewClientsDataSource() *schema.Resource {
return &schema.Resource{
ReadContext: readClientsForDataSource,
Description: "Data source to retrieve a list of Auth0 application clients with optional filtering.",
Schema: map[string]*schema.Schema{
"name_filter": {
Type: schema.TypeString,
Optional: true,
Description: "Filter clients by name (partial matches supported).",
},
"app_types": {
Type: schema.TypeSet,
Optional: true,
Description: "Filter clients by application types.",
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice(ValidAppTypes, false),
},
},
"is_first_party": {
Type: schema.TypeBool,
Optional: true,
Description: "Filter clients by first party status.",
},
"clients": {
Type: schema.TypeList,
Computed: true,
Description: "List of clients matching the filter criteria.",
Elem: &schema.Resource{
Schema: coreClientDataSourceSchema(),
},
},
},
}
}

func coreClientDataSourceSchema() map[string]*schema.Schema {
clientSchema := dataSourceSchema()

// Remove unused fields from the client schema.
fieldsToRemove := []string{
"client_aliases",
"logo_uri",
"oidc_conformant",
"oidc_backchannel_logout_urls",
"organization_usage",
"organization_require_behavior",
"cross_origin_auth",
"cross_origin_loc",
"custom_login_page_on",
"custom_login_page",
"form_template",
"require_pushed_authorization_requests",
"mobile",
"initiate_login_uri",
"native_social_login",
"refresh_token",
"signing_keys",
"encryption_key",
"sso",
"sso_disabled",
"jwt_configuration",
"addons",
"default_organization",
"compliance_level",
"require_proof_of_possession",
"token_endpoint_auth_method",
"signed_request_object",
"client_authentication_methods",
}

for _, field := range fieldsToRemove {
delete(clientSchema, field)
}

return clientSchema
}

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

nameFilter := data.Get("name_filter").(string)
appTypesSet := data.Get("app_types").(*schema.Set)
isFirstParty := data.Get("is_first_party").(bool)

appTypes := make([]string, 0, appTypesSet.Len())
for _, v := range appTypesSet.List() {
appTypes = append(appTypes, v.(string))
}

var clients []*management.Client

params := []management.RequestOption{
management.PerPage(100),
}

if len(appTypes) > 0 {
params = append(params, management.Parameter("app_type", strings.Join(appTypes, ",")))
}
if isFirstParty {
params = append(params, management.Parameter("is_first_party", "true"))
}

var page int
for {
// Add current page parameter.
params = append(params, management.Page(page))

list, err := api.Client.List(ctx, params...)
if err != nil {
return diag.FromErr(err)
}

for _, client := range list.Clients {
if nameFilter == "" || strings.Contains(client.GetName(), nameFilter) {
clients = append(clients, client)
}
}

if !list.HasNext() {
break
}

// Remove the page parameter and increment for next iteration.
params = params[:len(params)-1]
page++
}

filterID := generateFilterID(nameFilter, appTypes, isFirstParty)
data.SetId(filterID)

if err := flattenClientList(data, clients); err != nil {
return diag.FromErr(err)
}

return nil
}

func generateFilterID(nameFilter string, appTypes []string, isFirstParty bool) string {
h := sha256.New()
h.Write([]byte(fmt.Sprintf("%s-%v-%v", nameFilter, appTypes, isFirstParty)))
return fmt.Sprintf("clients-%x", h.Sum(nil))
}
Loading

0 comments on commit e158550

Please sign in to comment.