Skip to content

Commit df2370c

Browse files
feat(enterprise-scim): add github_enterprise_scim_users data source
List all SCIM users in an enterprise with optional filtering. Includes: - Data source implementation - Acceptance tests - Documentation
1 parent 09874ce commit df2370c

File tree

3 files changed

+337
-0
lines changed

3 files changed

+337
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/url"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
11+
)
12+
13+
func dataSourceGithubEnterpriseSCIMUsers() *schema.Resource {
14+
return &schema.Resource{
15+
Description: "Lookup SCIM users provisioned for a GitHub enterprise.",
16+
ReadContext: dataSourceGithubEnterpriseSCIMUsersRead,
17+
18+
Schema: map[string]*schema.Schema{
19+
"enterprise": {
20+
Description: "The enterprise slug.",
21+
Type: schema.TypeString,
22+
Required: true,
23+
},
24+
"filter": {
25+
Description: "Optional SCIM filter. See GitHub SCIM enterprise docs for supported filters.",
26+
Type: schema.TypeString,
27+
Optional: true,
28+
},
29+
"results_per_page": {
30+
Description: "Number of results per request (mapped to SCIM 'count'). Used while auto-fetching all pages.",
31+
Type: schema.TypeInt,
32+
Optional: true,
33+
Default: 100,
34+
ValidateFunc: validation.IntBetween(1, 100),
35+
},
36+
37+
"schemas": {
38+
Description: "SCIM response schemas.",
39+
Type: schema.TypeList,
40+
Computed: true,
41+
Elem: &schema.Schema{Type: schema.TypeString},
42+
},
43+
"total_results": {
44+
Description: "The total number of results returned by the SCIM endpoint.",
45+
Type: schema.TypeInt,
46+
Computed: true,
47+
},
48+
"start_index": {
49+
Description: "The startIndex from the first SCIM page.",
50+
Type: schema.TypeInt,
51+
Computed: true,
52+
},
53+
"items_per_page": {
54+
Description: "The itemsPerPage from the first SCIM page.",
55+
Type: schema.TypeInt,
56+
Computed: true,
57+
},
58+
"resources": {
59+
Description: "All SCIM users.",
60+
Type: schema.TypeList,
61+
Computed: true,
62+
Elem: &schema.Resource{
63+
Schema: enterpriseSCIMUserSchema(),
64+
},
65+
},
66+
},
67+
}
68+
}
69+
70+
func dataSourceGithubEnterpriseSCIMUsersRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
71+
client := meta.(*Owner).v3client
72+
73+
enterprise := d.Get("enterprise").(string)
74+
filter := d.Get("filter").(string)
75+
count := d.Get("results_per_page").(int)
76+
77+
users, first, err := enterpriseSCIMListAllUsers(ctx, client, enterprise, filter, count)
78+
if err != nil {
79+
return diag.FromErr(err)
80+
}
81+
82+
flat := make([]any, 0, len(users))
83+
for _, u := range users {
84+
flat = append(flat, flattenEnterpriseSCIMUser(u))
85+
}
86+
87+
id := fmt.Sprintf("%s/scim-users", enterprise)
88+
if filter != "" {
89+
id = fmt.Sprintf("%s?filter=%s", id, url.QueryEscape(filter))
90+
}
91+
92+
d.SetId(id)
93+
94+
if err := d.Set("schemas", first.Schemas); err != nil {
95+
return diag.FromErr(err)
96+
}
97+
if first.TotalResults != nil {
98+
if err := d.Set("total_results", *first.TotalResults); err != nil {
99+
return diag.FromErr(err)
100+
}
101+
}
102+
startIndex := 1
103+
if first.StartIndex != nil && *first.StartIndex > 0 {
104+
startIndex = *first.StartIndex
105+
}
106+
if err := d.Set("start_index", startIndex); err != nil {
107+
return diag.FromErr(err)
108+
}
109+
itemsPerPage := count
110+
if first.ItemsPerPage != nil && *first.ItemsPerPage > 0 {
111+
itemsPerPage = *first.ItemsPerPage
112+
}
113+
if err := d.Set("items_per_page", itemsPerPage); err != nil {
114+
return diag.FromErr(err)
115+
}
116+
if err := d.Set("resources", flat); err != nil {
117+
return diag.FromErr(err)
118+
}
119+
120+
return nil
121+
}
122+
123+
func enterpriseSCIMUserSchema() map[string]*schema.Schema {
124+
return map[string]*schema.Schema{
125+
"schemas": {
126+
Type: schema.TypeList,
127+
Computed: true,
128+
Description: "SCIM schemas for this user.",
129+
Elem: &schema.Schema{Type: schema.TypeString},
130+
},
131+
"id": {
132+
Type: schema.TypeString,
133+
Computed: true,
134+
Description: "The SCIM user ID.",
135+
},
136+
"external_id": {
137+
Type: schema.TypeString,
138+
Computed: true,
139+
Description: "The external ID for the user.",
140+
},
141+
"user_name": {
142+
Type: schema.TypeString,
143+
Computed: true,
144+
Description: "The SCIM userName.",
145+
},
146+
"display_name": {
147+
Type: schema.TypeString,
148+
Computed: true,
149+
Description: "The SCIM displayName.",
150+
},
151+
"active": {
152+
Type: schema.TypeBool,
153+
Computed: true,
154+
Description: "Whether the user is active.",
155+
},
156+
"name": {
157+
Type: schema.TypeList,
158+
Computed: true,
159+
Description: "User name object.",
160+
Elem: &schema.Resource{Schema: enterpriseSCIMUserNameSchema()},
161+
},
162+
"emails": {
163+
Type: schema.TypeList,
164+
Computed: true,
165+
Description: "User emails.",
166+
Elem: &schema.Resource{Schema: enterpriseSCIMUserEmailSchema()},
167+
},
168+
"roles": {
169+
Type: schema.TypeList,
170+
Computed: true,
171+
Description: "User roles.",
172+
Elem: &schema.Resource{Schema: enterpriseSCIMUserRoleSchema()},
173+
},
174+
"meta": {
175+
Type: schema.TypeList,
176+
Computed: true,
177+
Description: "Resource metadata.",
178+
Elem: &schema.Resource{Schema: enterpriseSCIMMetaSchema()},
179+
},
180+
}
181+
}
182+
183+
func enterpriseSCIMUserNameSchema() map[string]*schema.Schema {
184+
return map[string]*schema.Schema{
185+
"formatted": {
186+
Type: schema.TypeString,
187+
Computed: true,
188+
Description: "Formatted name.",
189+
},
190+
"family_name": {
191+
Type: schema.TypeString,
192+
Computed: true,
193+
Description: "Family name.",
194+
},
195+
"given_name": {
196+
Type: schema.TypeString,
197+
Computed: true,
198+
Description: "Given name.",
199+
},
200+
"middle_name": {
201+
Type: schema.TypeString,
202+
Computed: true,
203+
Description: "Middle name.",
204+
},
205+
}
206+
}
207+
208+
func enterpriseSCIMUserEmailSchema() map[string]*schema.Schema {
209+
return map[string]*schema.Schema{
210+
"value": {
211+
Type: schema.TypeString,
212+
Computed: true,
213+
Description: "Email address.",
214+
},
215+
"type": {
216+
Type: schema.TypeString,
217+
Computed: true,
218+
Description: "Email type.",
219+
},
220+
"primary": {
221+
Type: schema.TypeBool,
222+
Computed: true,
223+
Description: "Whether this email is primary.",
224+
},
225+
}
226+
}
227+
228+
func enterpriseSCIMUserRoleSchema() map[string]*schema.Schema {
229+
return map[string]*schema.Schema{
230+
"value": {
231+
Type: schema.TypeString,
232+
Computed: true,
233+
Description: "Role value.",
234+
},
235+
"display": {
236+
Type: schema.TypeString,
237+
Computed: true,
238+
Description: "Role display.",
239+
},
240+
"type": {
241+
Type: schema.TypeString,
242+
Computed: true,
243+
Description: "Role type.",
244+
},
245+
"primary": {
246+
Type: schema.TypeBool,
247+
Computed: true,
248+
Description: "Whether this role is primary.",
249+
},
250+
}
251+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package github
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
8+
)
9+
10+
func TestAccGithubEnterpriseSCIMUsersDataSource(t *testing.T) {
11+
t.Run("lists users without error", func(t *testing.T) {
12+
resource.Test(t, resource.TestCase{
13+
PreCheck: func() { skipUnlessEnterprise(t) },
14+
ProviderFactories: providerFactories,
15+
Steps: []resource.TestStep{{
16+
Config: fmt.Sprintf(`
17+
data "github_enterprise_scim_users" "test" {
18+
enterprise = "%s"
19+
}
20+
`, testAccConf.enterpriseSlug),
21+
Check: resource.ComposeTestCheckFunc(
22+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_users.test", "id"),
23+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_users.test", "total_results"),
24+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_users.test", "schemas.#"),
25+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_users.test", "resources.#"),
26+
),
27+
}},
28+
})
29+
})
30+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
layout: "github"
3+
page_title: "GitHub: github_enterprise_scim_users"
4+
description: |-
5+
Get SCIM users provisioned for a GitHub enterprise.
6+
---
7+
8+
# github_enterprise_scim_users
9+
10+
Use this data source to retrieve SCIM users provisioned for a GitHub enterprise.
11+
12+
## Example Usage
13+
14+
```hcl
15+
data "github_enterprise_scim_users" "example" {
16+
enterprise = "example-co"
17+
}
18+
```
19+
20+
## Argument Reference
21+
22+
* `enterprise` - (Required) The enterprise slug.
23+
* `filter` - (Optional) SCIM filter string.
24+
* `results_per_page` - (Optional) Page size used while auto-fetching all pages (mapped to SCIM `count`).
25+
26+
### Notes on `filter`
27+
28+
`filter` is passed to the GitHub SCIM API as-is (server-side filtering). It is **not** a Terraform expression and it does **not** understand provider schema paths such as `name[0].family_name`.
29+
30+
GitHub supports **only one** filter expression and only for these attributes on the enterprise `Users` listing endpoint:
31+
32+
* `userName`
33+
* `externalId`
34+
* `id`
35+
* `displayName`
36+
37+
Examples:
38+
39+
```hcl
40+
filter = "userName eq \"E012345\""
41+
```
42+
43+
```hcl
44+
filter = "externalId eq \"9138790-10932-109120392-12321\""
45+
```
46+
47+
If you need to filter by other values that only exist in the Terraform schema (for example `name[0].family_name`), retrieve the users and filter locally in Terraform.
48+
49+
## Attributes Reference
50+
51+
* `schemas` - SCIM response schemas.
52+
* `total_results` - Total number of SCIM users.
53+
* `start_index` - Start index from the first page.
54+
* `items_per_page` - Items per page from the first page.
55+
* `resources` - List of SCIM users. Each entry includes:
56+
* `schemas`, `id`, `external_id`, `user_name`, `display_name`, `active`, `name`, `emails`, `roles`, `meta`.

0 commit comments

Comments
 (0)