Skip to content

Commit e158550

Browse files
Add "auth0_clients" data source for listing multiple clients with filtering (#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 <[email protected]>
1 parent 900c692 commit e158550

File tree

9 files changed

+1448
-13
lines changed

9 files changed

+1448
-13
lines changed

docs/data-sources/clients.md

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
page_title: "Data Source: auth0_clients"
3+
description: |-
4+
Data source to retrieve a list of Auth0 application clients with optional filtering.
5+
---
6+
7+
# Data Source: auth0_clients
8+
9+
Data source to retrieve a list of Auth0 application clients with optional filtering.
10+
11+
## Example Usage
12+
13+
```terraform
14+
# Auth0 clients with "External" in the name
15+
data "auth0_clients" "external_apps" {
16+
name_filter = "External"
17+
}
18+
19+
# Auth0 clients filtered by non_interactive or spa app type
20+
data "auth0_clients" "m2m_apps" {
21+
app_types = ["non_interactive", "spa"]
22+
}
23+
24+
# Auth0 clients filtered by is_first_party equal to true
25+
data "auth0_clients" "first_party_apps" {
26+
is_first_party = true
27+
}
28+
```
29+
30+
<!-- schema generated by tfplugindocs -->
31+
## Schema
32+
33+
### Optional
34+
35+
- `app_types` (Set of String) Filter clients by application types.
36+
- `is_first_party` (Boolean) Filter clients by first party status.
37+
- `name_filter` (String) Filter clients by name (partial matches supported).
38+
39+
### Read-Only
40+
41+
- `clients` (List of Object) List of clients matching the filter criteria. (see [below for nested schema](#nestedatt--clients))
42+
- `id` (String) The ID of this resource.
43+
44+
<a id="nestedatt--clients"></a>
45+
### Nested Schema for `clients`
46+
47+
Read-Only:
48+
49+
- `allowed_clients` (List of String)
50+
- `allowed_logout_urls` (List of String)
51+
- `allowed_origins` (List of String)
52+
- `app_type` (String)
53+
- `callbacks` (List of String)
54+
- `client_id` (String)
55+
- `client_metadata` (Map of String)
56+
- `client_secret` (String)
57+
- `description` (String)
58+
- `grant_types` (List of String)
59+
- `is_first_party` (Boolean)
60+
- `is_token_endpoint_ip_header_trusted` (Boolean)
61+
- `name` (String)
62+
- `web_origins` (List of String)
63+
64+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Auth0 clients with "External" in the name
2+
data "auth0_clients" "external_apps" {
3+
name_filter = "External"
4+
}
5+
6+
# Auth0 clients filtered by non_interactive or spa app type
7+
data "auth0_clients" "m2m_apps" {
8+
app_types = ["non_interactive", "spa"]
9+
}
10+
11+
# Auth0 clients filtered by is_first_party equal to true
12+
data "auth0_clients" "first_party_apps" {
13+
is_first_party = true
14+
}

internal/acctest/http_recorder.go

+34-5
Original file line numberDiff line numberDiff line change
@@ -108,27 +108,56 @@ func redactDomain(i *cassette.Interaction, domain string) {
108108
}
109109

110110
func redactSensitiveDataInClient(t *testing.T, i *cassette.Interaction, domain string) {
111-
create := i.Request.URL == "https://"+domain+"/api/v2/clients" &&
111+
baseURL := "https://" + domain + "/api/v2/clients"
112+
urlPath := strings.Split(i.Request.URL, "?")[0] // Strip query params.
113+
114+
create := i.Request.URL == baseURL &&
112115
i.Request.Method == http.MethodPost
113116

114-
read := strings.Contains(i.Request.URL, "https://"+domain+"/api/v2/clients/") &&
117+
readList := urlPath == baseURL &&
118+
i.Request.Method == http.MethodGet
119+
120+
readOne := strings.Contains(i.Request.URL, baseURL+"/") &&
115121
!strings.Contains(i.Request.URL, "credentials") &&
116122
i.Request.Method == http.MethodGet
117123

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

122-
if create || read || update {
128+
if create || readList || readOne || update {
123129
if i.Response.Code == http.StatusNotFound {
124130
return
125131
}
126132

133+
redacted := "[REDACTED]"
134+
135+
// Handle list response.
136+
if readList {
137+
var response management.ClientList
138+
err := json.Unmarshal([]byte(i.Response.Body), &response)
139+
require.NoError(t, err)
140+
141+
for _, client := range response.Clients {
142+
client.SigningKeys = []map[string]string{
143+
{"cert": redacted},
144+
}
145+
if client.GetClientSecret() != "" {
146+
client.ClientSecret = &redacted
147+
}
148+
}
149+
150+
responseBody, err := json.Marshal(response)
151+
require.NoError(t, err)
152+
i.Response.Body = string(responseBody)
153+
return
154+
}
155+
156+
// Handle single client response.
127157
var client management.Client
128158
err := json.Unmarshal([]byte(i.Response.Body), &client)
129159
require.NoError(t, err)
130160

131-
redacted := "[REDACTED]"
132161
client.SigningKeys = []map[string]string{
133162
{"cert": redacted},
134163
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/auth0/go-auth0/management"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
13+
14+
"github.com/auth0/terraform-provider-auth0/internal/config"
15+
)
16+
17+
// NewClientsDataSource will return a new auth0_clients data source.
18+
func NewClientsDataSource() *schema.Resource {
19+
return &schema.Resource{
20+
ReadContext: readClientsForDataSource,
21+
Description: "Data source to retrieve a list of Auth0 application clients with optional filtering.",
22+
Schema: map[string]*schema.Schema{
23+
"name_filter": {
24+
Type: schema.TypeString,
25+
Optional: true,
26+
Description: "Filter clients by name (partial matches supported).",
27+
},
28+
"app_types": {
29+
Type: schema.TypeSet,
30+
Optional: true,
31+
Description: "Filter clients by application types.",
32+
Elem: &schema.Schema{
33+
Type: schema.TypeString,
34+
ValidateFunc: validation.StringInSlice(ValidAppTypes, false),
35+
},
36+
},
37+
"is_first_party": {
38+
Type: schema.TypeBool,
39+
Optional: true,
40+
Description: "Filter clients by first party status.",
41+
},
42+
"clients": {
43+
Type: schema.TypeList,
44+
Computed: true,
45+
Description: "List of clients matching the filter criteria.",
46+
Elem: &schema.Resource{
47+
Schema: coreClientDataSourceSchema(),
48+
},
49+
},
50+
},
51+
}
52+
}
53+
54+
func coreClientDataSourceSchema() map[string]*schema.Schema {
55+
clientSchema := dataSourceSchema()
56+
57+
// Remove unused fields from the client schema.
58+
fieldsToRemove := []string{
59+
"client_aliases",
60+
"logo_uri",
61+
"oidc_conformant",
62+
"oidc_backchannel_logout_urls",
63+
"organization_usage",
64+
"organization_require_behavior",
65+
"cross_origin_auth",
66+
"cross_origin_loc",
67+
"custom_login_page_on",
68+
"custom_login_page",
69+
"form_template",
70+
"require_pushed_authorization_requests",
71+
"mobile",
72+
"initiate_login_uri",
73+
"native_social_login",
74+
"refresh_token",
75+
"signing_keys",
76+
"encryption_key",
77+
"sso",
78+
"sso_disabled",
79+
"jwt_configuration",
80+
"addons",
81+
"default_organization",
82+
"compliance_level",
83+
"require_proof_of_possession",
84+
"token_endpoint_auth_method",
85+
"signed_request_object",
86+
"client_authentication_methods",
87+
}
88+
89+
for _, field := range fieldsToRemove {
90+
delete(clientSchema, field)
91+
}
92+
93+
return clientSchema
94+
}
95+
96+
func readClientsForDataSource(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
97+
api := meta.(*config.Config).GetAPI()
98+
99+
nameFilter := data.Get("name_filter").(string)
100+
appTypesSet := data.Get("app_types").(*schema.Set)
101+
isFirstParty := data.Get("is_first_party").(bool)
102+
103+
appTypes := make([]string, 0, appTypesSet.Len())
104+
for _, v := range appTypesSet.List() {
105+
appTypes = append(appTypes, v.(string))
106+
}
107+
108+
var clients []*management.Client
109+
110+
params := []management.RequestOption{
111+
management.PerPage(100),
112+
}
113+
114+
if len(appTypes) > 0 {
115+
params = append(params, management.Parameter("app_type", strings.Join(appTypes, ",")))
116+
}
117+
if isFirstParty {
118+
params = append(params, management.Parameter("is_first_party", "true"))
119+
}
120+
121+
var page int
122+
for {
123+
// Add current page parameter.
124+
params = append(params, management.Page(page))
125+
126+
list, err := api.Client.List(ctx, params...)
127+
if err != nil {
128+
return diag.FromErr(err)
129+
}
130+
131+
for _, client := range list.Clients {
132+
if nameFilter == "" || strings.Contains(client.GetName(), nameFilter) {
133+
clients = append(clients, client)
134+
}
135+
}
136+
137+
if !list.HasNext() {
138+
break
139+
}
140+
141+
// Remove the page parameter and increment for next iteration.
142+
params = params[:len(params)-1]
143+
page++
144+
}
145+
146+
filterID := generateFilterID(nameFilter, appTypes, isFirstParty)
147+
data.SetId(filterID)
148+
149+
if err := flattenClientList(data, clients); err != nil {
150+
return diag.FromErr(err)
151+
}
152+
153+
return nil
154+
}
155+
156+
func generateFilterID(nameFilter string, appTypes []string, isFirstParty bool) string {
157+
h := sha256.New()
158+
h.Write([]byte(fmt.Sprintf("%s-%v-%v", nameFilter, appTypes, isFirstParty)))
159+
return fmt.Sprintf("clients-%x", h.Sum(nil))
160+
}

0 commit comments

Comments
 (0)