Skip to content

Commit f785b45

Browse files
feat: add Enterprise SCIM data sources (groups and users)
Add four new data sources for querying SCIM provisioning information in GitHub Enterprise: - github_enterprise_scim_group: Lookup single SCIM group by ID - github_enterprise_scim_groups: List all SCIM groups with filtering - github_enterprise_scim_user: Lookup single SCIM user by ID - github_enterprise_scim_users: List all SCIM users with filtering Features: - Automatic pagination for list endpoints - SCIM filter support (server-side filtering) - Full attribute exposure: schemas, meta, members, emails, roles - Proper error handling with diag.Errorf Testing: - Acceptance tests with t.Run() sub-tests pattern - Inline configs and checks following project conventions - Uses testAccConf.enterpriseSlug and skipUnlessEnterprise(t) Documentation: - Complete docs with examples and filter usage notes - Attribute reference for all fields
1 parent 15fff78 commit f785b45

14 files changed

Lines changed: 1220 additions & 0 deletions
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
)
10+
11+
func dataSourceGithubEnterpriseSCIMGroup() *schema.Resource {
12+
return &schema.Resource{
13+
Description: "Lookup SCIM provisioning information for a single GitHub enterprise group.",
14+
ReadContext: dataSourceGithubEnterpriseSCIMGroupRead,
15+
16+
Schema: map[string]*schema.Schema{
17+
"enterprise": {
18+
Description: "The enterprise slug.",
19+
Type: schema.TypeString,
20+
Required: true,
21+
},
22+
"scim_group_id": {
23+
Description: "The SCIM group ID.",
24+
Type: schema.TypeString,
25+
Required: true,
26+
},
27+
28+
"schemas": {
29+
Type: schema.TypeList,
30+
Computed: true,
31+
Description: "SCIM schemas for this group.",
32+
Elem: &schema.Schema{Type: schema.TypeString},
33+
},
34+
"id": {
35+
Type: schema.TypeString,
36+
Computed: true,
37+
Description: "The SCIM group ID.",
38+
},
39+
"external_id": {
40+
Type: schema.TypeString,
41+
Computed: true,
42+
Description: "The external ID for the group.",
43+
},
44+
"display_name": {
45+
Type: schema.TypeString,
46+
Computed: true,
47+
Description: "The SCIM group displayName.",
48+
},
49+
"members": {
50+
Type: schema.TypeList,
51+
Computed: true,
52+
Description: "Group members.",
53+
Elem: &schema.Resource{Schema: enterpriseSCIMGroupMemberSchema()},
54+
},
55+
"meta": {
56+
Type: schema.TypeList,
57+
Computed: true,
58+
Description: "Resource metadata.",
59+
Elem: &schema.Resource{Schema: enterpriseSCIMMetaSchema()},
60+
},
61+
},
62+
}
63+
}
64+
65+
func dataSourceGithubEnterpriseSCIMGroupRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
66+
client := meta.(*Owner).v3client
67+
68+
enterprise := d.Get("enterprise").(string)
69+
scimGroupID := d.Get("scim_group_id").(string)
70+
71+
group, _, err := client.Enterprise.GetProvisionedSCIMGroup(ctx, enterprise, scimGroupID, nil)
72+
if err != nil {
73+
return diag.FromErr(err)
74+
}
75+
76+
d.SetId(fmt.Sprintf("%s/%s", enterprise, scimGroupID))
77+
78+
if err := d.Set("schemas", group.Schemas); err != nil {
79+
return diag.Errorf("error setting schemas: %s", err)
80+
}
81+
if group.ID != nil {
82+
if err := d.Set("id", *group.ID); err != nil {
83+
return diag.Errorf("error setting id: %s", err)
84+
}
85+
}
86+
if group.ExternalID != nil {
87+
if err := d.Set("external_id", *group.ExternalID); err != nil {
88+
return diag.Errorf("error setting external_id: %s", err)
89+
}
90+
}
91+
if group.DisplayName != nil {
92+
if err := d.Set("display_name", *group.DisplayName); err != nil {
93+
return diag.Errorf("error setting display_name: %s", err)
94+
}
95+
}
96+
if err := d.Set("members", flattenEnterpriseSCIMGroupMembers(group.Members)); err != nil {
97+
return diag.Errorf("error setting members: %s", err)
98+
}
99+
if err := d.Set("meta", flattenEnterpriseSCIMMeta(group.Meta)); err != nil {
100+
return diag.Errorf("error setting meta: %s", err)
101+
}
102+
103+
return nil
104+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 TestAccGithubEnterpriseSCIMGroupDataSource(t *testing.T) {
11+
t.Run("reads group 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_groups" "all" {
18+
enterprise = "%s"
19+
}
20+
21+
data "github_enterprise_scim_group" "test" {
22+
enterprise = "%[1]s"
23+
scim_group_id = data.github_enterprise_scim_groups.all.resources[0].id
24+
}
25+
`, testAccConf.enterpriseSlug),
26+
Check: resource.ComposeTestCheckFunc(
27+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_group.test", "id"),
28+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_group.test", "display_name"),
29+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_group.test", "schemas.#"),
30+
),
31+
}},
32+
})
33+
})
34+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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 dataSourceGithubEnterpriseSCIMGroups() *schema.Resource {
14+
return &schema.Resource{
15+
Description: "Lookup SCIM groups provisioned for a GitHub enterprise.",
16+
ReadContext: dataSourceGithubEnterpriseSCIMGroupsRead,
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 groups.",
60+
Type: schema.TypeList,
61+
Computed: true,
62+
Elem: &schema.Resource{
63+
Schema: enterpriseSCIMGroupSchema(),
64+
},
65+
},
66+
},
67+
}
68+
}
69+
70+
func dataSourceGithubEnterpriseSCIMGroupsRead(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+
groups, first, err := enterpriseSCIMListAllGroups(ctx, client, enterprise, filter, count)
78+
if err != nil {
79+
return diag.FromErr(err)
80+
}
81+
82+
flat := make([]any, 0, len(groups))
83+
for _, g := range groups {
84+
flat = append(flat, flattenEnterpriseSCIMGroup(g))
85+
}
86+
87+
id := fmt.Sprintf("%s/scim-groups", 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.Errorf("error setting schemas: %s", err)
96+
}
97+
if first.TotalResults != nil {
98+
if err := d.Set("total_results", *first.TotalResults); err != nil {
99+
return diag.Errorf("error setting total_results: %s", err)
100+
}
101+
}
102+
if first.StartIndex != nil && *first.StartIndex > 0 {
103+
if err := d.Set("start_index", *first.StartIndex); err != nil {
104+
return diag.Errorf("error setting start_index: %s", err)
105+
}
106+
} else {
107+
if err := d.Set("start_index", 1); err != nil {
108+
return diag.Errorf("error setting start_index: %s", err)
109+
}
110+
}
111+
if first.ItemsPerPage != nil && *first.ItemsPerPage > 0 {
112+
if err := d.Set("items_per_page", *first.ItemsPerPage); err != nil {
113+
return diag.Errorf("error setting items_per_page: %s", err)
114+
}
115+
} else {
116+
if err := d.Set("items_per_page", count); err != nil {
117+
return diag.Errorf("error setting items_per_page: %s", err)
118+
}
119+
}
120+
if err := d.Set("resources", flat); err != nil {
121+
return diag.Errorf("error setting resources: %s", err)
122+
}
123+
124+
return nil
125+
}
126+
127+
func enterpriseSCIMMetaSchema() map[string]*schema.Schema {
128+
return map[string]*schema.Schema{
129+
"resource_type": {
130+
Type: schema.TypeString,
131+
Computed: true,
132+
Description: "The SCIM resource type.",
133+
},
134+
"created": {
135+
Type: schema.TypeString,
136+
Computed: true,
137+
Description: "The creation timestamp.",
138+
},
139+
"last_modified": {
140+
Type: schema.TypeString,
141+
Computed: true,
142+
Description: "The lastModified timestamp.",
143+
},
144+
"location": {
145+
Type: schema.TypeString,
146+
Computed: true,
147+
Description: "The resource location.",
148+
},
149+
}
150+
}
151+
152+
func enterpriseSCIMGroupSchema() map[string]*schema.Schema {
153+
return map[string]*schema.Schema{
154+
"schemas": {
155+
Type: schema.TypeList,
156+
Computed: true,
157+
Description: "SCIM schemas for this group.",
158+
Elem: &schema.Schema{Type: schema.TypeString},
159+
},
160+
"id": {
161+
Type: schema.TypeString,
162+
Computed: true,
163+
Description: "The SCIM group ID.",
164+
},
165+
"external_id": {
166+
Type: schema.TypeString,
167+
Computed: true,
168+
Description: "The external ID for the group.",
169+
},
170+
"display_name": {
171+
Type: schema.TypeString,
172+
Computed: true,
173+
Description: "The SCIM group displayName.",
174+
},
175+
"members": {
176+
Type: schema.TypeList,
177+
Computed: true,
178+
Description: "Group members.",
179+
Elem: &schema.Resource{Schema: enterpriseSCIMGroupMemberSchema()},
180+
},
181+
"meta": {
182+
Type: schema.TypeList,
183+
Computed: true,
184+
Description: "Resource metadata.",
185+
Elem: &schema.Resource{Schema: enterpriseSCIMMetaSchema()},
186+
},
187+
}
188+
}
189+
190+
func enterpriseSCIMGroupMemberSchema() map[string]*schema.Schema {
191+
return map[string]*schema.Schema{
192+
"value": {
193+
Type: schema.TypeString,
194+
Computed: true,
195+
Description: "Member identifier.",
196+
},
197+
"ref": {
198+
Type: schema.TypeString,
199+
Computed: true,
200+
Description: "Member reference URL.",
201+
},
202+
"display_name": {
203+
Type: schema.TypeString,
204+
Computed: true,
205+
Description: "Member display name.",
206+
},
207+
}
208+
}
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 TestAccGithubEnterpriseSCIMGroupsDataSource(t *testing.T) {
11+
t.Run("lists groups 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_groups" "test" {
18+
enterprise = "%s"
19+
}
20+
`, testAccConf.enterpriseSlug),
21+
Check: resource.ComposeTestCheckFunc(
22+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_groups.test", "id"),
23+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_groups.test", "total_results"),
24+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_groups.test", "schemas.#"),
25+
resource.TestCheckResourceAttrSet("data.github_enterprise_scim_groups.test", "resources.#"),
26+
),
27+
}},
28+
})
29+
})
30+
}

0 commit comments

Comments
 (0)