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

Add "auth0_clients" data source for listing multiple clients with filtering #1080

Merged
merged 6 commits into from
Nov 26, 2024
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
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
Loading