Skip to content

Commit 37b2c86

Browse files
feat: github enterprise scim data sources
1 parent 11a7bfb commit 37b2c86

13 files changed

+1495
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
"excluded_attributes": {
28+
Description: "Optional SCIM excludedAttributes query parameter.",
29+
Type: schema.TypeString,
30+
Optional: true,
31+
},
32+
33+
"schemas": {
34+
Type: schema.TypeList,
35+
Computed: true,
36+
Description: "SCIM schemas for this group.",
37+
Elem: &schema.Schema{Type: schema.TypeString},
38+
},
39+
"id": {
40+
Type: schema.TypeString,
41+
Computed: true,
42+
Description: "The SCIM group ID.",
43+
},
44+
"external_id": {
45+
Type: schema.TypeString,
46+
Computed: true,
47+
Description: "The external ID for the group.",
48+
},
49+
"display_name": {
50+
Type: schema.TypeString,
51+
Computed: true,
52+
Description: "The SCIM group displayName.",
53+
},
54+
"members": {
55+
Type: schema.TypeList,
56+
Computed: true,
57+
Description: "Group members.",
58+
Elem: &schema.Resource{Schema: enterpriseSCIMGroupMemberSchema()},
59+
},
60+
"meta": {
61+
Type: schema.TypeList,
62+
Computed: true,
63+
Description: "Resource metadata.",
64+
Elem: &schema.Resource{Schema: enterpriseSCIMMetaSchema()},
65+
},
66+
},
67+
}
68+
}
69+
70+
func dataSourceGithubEnterpriseSCIMGroupRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
71+
client := meta.(*Owner).v3client
72+
73+
enterprise := d.Get("enterprise").(string)
74+
scimGroupID := d.Get("scim_group_id").(string)
75+
excluded := d.Get("excluded_attributes").(string)
76+
77+
path := fmt.Sprintf("scim/v2/enterprises/%s/Groups/%s", enterprise, scimGroupID)
78+
urlStr, err := enterpriseSCIMListURL(path, enterpriseSCIMListOptions{ExcludedAttributes: excluded})
79+
if err != nil {
80+
return diag.FromErr(err)
81+
}
82+
83+
group := enterpriseSCIMGroup{}
84+
_, err = enterpriseSCIMGet(ctx, client, urlStr, &group)
85+
if err != nil {
86+
return diag.FromErr(err)
87+
}
88+
89+
d.SetId(fmt.Sprintf("%s/%s", enterprise, scimGroupID))
90+
91+
_ = d.Set("schemas", group.Schemas)
92+
_ = d.Set("id", group.ID)
93+
_ = d.Set("external_id", group.ExternalID)
94+
_ = d.Set("display_name", group.DisplayName)
95+
_ = d.Set("members", flattenEnterpriseSCIMGroupMembers(group.Members))
96+
_ = d.Set("meta", flattenEnterpriseSCIMMeta(group.Meta))
97+
98+
return nil
99+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"testing"
8+
9+
gh "github.com/google/go-github/v67/github"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
)
12+
13+
func TestDataSourceGithubEnterpriseSCIMGroupRead(t *testing.T) {
14+
ts := githubApiMock([]*mockResponse{
15+
{
16+
ExpectedUri: "/scim/v2/enterprises/ent/Groups/g1",
17+
ExpectedHeaders: map[string]string{
18+
"Accept": enterpriseSCIMAcceptHeader,
19+
},
20+
StatusCode: 200,
21+
ResponseBody: `{
22+
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
23+
"id": "g1",
24+
"externalId": "eg1",
25+
"displayName": "Group One",
26+
"members": [{"value": "u1", "$ref": "https://example.test/u1", "display": "user1"}],
27+
"meta": {"resourceType": "Group", "created": "2020-01-01T00:00:00Z"}
28+
}`,
29+
},
30+
})
31+
defer ts.Close()
32+
33+
httpClient := &http.Client{Transport: http.DefaultTransport}
34+
client := gh.NewClient(httpClient)
35+
baseURL, _ := url.Parse(ts.URL + "/")
36+
client.BaseURL = baseURL
37+
38+
owner := &Owner{v3client: client}
39+
40+
r := dataSourceGithubEnterpriseSCIMGroup()
41+
d := schema.TestResourceDataRaw(t, r.Schema, map[string]any{
42+
"enterprise": "ent",
43+
"scim_group_id": "g1",
44+
"excluded_attributes": "",
45+
})
46+
47+
diags := dataSourceGithubEnterpriseSCIMGroupRead(context.Background(), d, owner)
48+
if len(diags) > 0 {
49+
t.Fatalf("unexpected diagnostics: %#v", diags)
50+
}
51+
52+
if got := d.Get("id").(string); got != "g1" {
53+
t.Fatalf("expected id g1, got %q", got)
54+
}
55+
members := d.Get("members").([]any)
56+
if len(members) != 1 {
57+
t.Fatalf("expected 1 member, got %d", len(members))
58+
}
59+
m0 := members[0].(map[string]any)
60+
if m0["ref"].(string) != "https://example.test/u1" {
61+
t.Fatalf("expected ref to be set, got %v", m0["ref"])
62+
}
63+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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+
"excluded_attributes": {
30+
Description: "Optional SCIM excludedAttributes query parameter.",
31+
Type: schema.TypeString,
32+
Optional: true,
33+
},
34+
"results_per_page": {
35+
Description: "Number of results per request (mapped to SCIM 'count'). Used while auto-fetching all pages.",
36+
Type: schema.TypeInt,
37+
Optional: true,
38+
Default: 100,
39+
ValidateFunc: validation.IntBetween(1, 100),
40+
},
41+
42+
"schemas": {
43+
Description: "SCIM response schemas.",
44+
Type: schema.TypeList,
45+
Computed: true,
46+
Elem: &schema.Schema{Type: schema.TypeString},
47+
},
48+
"total_results": {
49+
Description: "The total number of results returned by the SCIM endpoint.",
50+
Type: schema.TypeInt,
51+
Computed: true,
52+
},
53+
"start_index": {
54+
Description: "The startIndex from the first SCIM page.",
55+
Type: schema.TypeInt,
56+
Computed: true,
57+
},
58+
"items_per_page": {
59+
Description: "The itemsPerPage from the first SCIM page.",
60+
Type: schema.TypeInt,
61+
Computed: true,
62+
},
63+
"resources": {
64+
Description: "All SCIM groups.",
65+
Type: schema.TypeList,
66+
Computed: true,
67+
Elem: &schema.Resource{
68+
Schema: enterpriseSCIMGroupSchema(),
69+
},
70+
},
71+
},
72+
}
73+
}
74+
75+
func dataSourceGithubEnterpriseSCIMGroupsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
76+
client := meta.(*Owner).v3client
77+
78+
enterprise := d.Get("enterprise").(string)
79+
filter := d.Get("filter").(string)
80+
excluded := d.Get("excluded_attributes").(string)
81+
count := d.Get("results_per_page").(int)
82+
83+
groups, first, err := enterpriseSCIMListAllGroups(ctx, client, enterprise, filter, excluded, count)
84+
if err != nil {
85+
return diag.FromErr(err)
86+
}
87+
88+
flat := make([]any, 0, len(groups))
89+
for _, g := range groups {
90+
flat = append(flat, flattenEnterpriseSCIMGroup(g))
91+
}
92+
93+
id := fmt.Sprintf("%s/scim-groups", enterprise)
94+
if filter != "" {
95+
id = fmt.Sprintf("%s?filter=%s", id, url.QueryEscape(filter))
96+
}
97+
if excluded != "" {
98+
if filter == "" {
99+
id = fmt.Sprintf("%s?excluded_attributes=%s", id, url.QueryEscape(excluded))
100+
} else {
101+
id = fmt.Sprintf("%s&excluded_attributes=%s", id, url.QueryEscape(excluded))
102+
}
103+
}
104+
105+
d.SetId(id)
106+
107+
_ = d.Set("schemas", first.Schemas)
108+
_ = d.Set("total_results", first.TotalResults)
109+
if first.StartIndex > 0 {
110+
_ = d.Set("start_index", first.StartIndex)
111+
} else {
112+
_ = d.Set("start_index", 1)
113+
}
114+
if first.ItemsPerPage > 0 {
115+
_ = d.Set("items_per_page", first.ItemsPerPage)
116+
} else {
117+
_ = d.Set("items_per_page", count)
118+
}
119+
if err := d.Set("resources", flat); err != nil {
120+
return diag.FromErr(fmt.Errorf("error setting resources: %w", err))
121+
}
122+
123+
return nil
124+
}
125+
126+
func enterpriseSCIMMetaSchema() map[string]*schema.Schema {
127+
return map[string]*schema.Schema{
128+
"resource_type": {
129+
Type: schema.TypeString,
130+
Computed: true,
131+
Description: "The SCIM resource type.",
132+
},
133+
"created": {
134+
Type: schema.TypeString,
135+
Computed: true,
136+
Description: "The creation timestamp.",
137+
},
138+
"last_modified": {
139+
Type: schema.TypeString,
140+
Computed: true,
141+
Description: "The lastModified timestamp.",
142+
},
143+
"location": {
144+
Type: schema.TypeString,
145+
Computed: true,
146+
Description: "The resource location.",
147+
},
148+
"version": {
149+
Type: schema.TypeString,
150+
Computed: true,
151+
Description: "The resource version.",
152+
},
153+
"etag": {
154+
Type: schema.TypeString,
155+
Computed: true,
156+
Description: "The resource eTag.",
157+
},
158+
"password_changed_at": {
159+
Type: schema.TypeString,
160+
Computed: true,
161+
Description: "The password changed at timestamp (if present).",
162+
},
163+
}
164+
}
165+
166+
func enterpriseSCIMGroupSchema() map[string]*schema.Schema {
167+
return map[string]*schema.Schema{
168+
"schemas": {
169+
Type: schema.TypeList,
170+
Computed: true,
171+
Description: "SCIM schemas for this group.",
172+
Elem: &schema.Schema{Type: schema.TypeString},
173+
},
174+
"id": {
175+
Type: schema.TypeString,
176+
Computed: true,
177+
Description: "The SCIM group ID.",
178+
},
179+
"external_id": {
180+
Type: schema.TypeString,
181+
Computed: true,
182+
Description: "The external ID for the group.",
183+
},
184+
"display_name": {
185+
Type: schema.TypeString,
186+
Computed: true,
187+
Description: "The SCIM group displayName.",
188+
},
189+
"members": {
190+
Type: schema.TypeList,
191+
Computed: true,
192+
Description: "Group members.",
193+
Elem: &schema.Resource{Schema: enterpriseSCIMGroupMemberSchema()},
194+
},
195+
"meta": {
196+
Type: schema.TypeList,
197+
Computed: true,
198+
Description: "Resource metadata.",
199+
Elem: &schema.Resource{Schema: enterpriseSCIMMetaSchema()},
200+
},
201+
}
202+
}
203+
204+
func enterpriseSCIMGroupMemberSchema() map[string]*schema.Schema {
205+
return map[string]*schema.Schema{
206+
"value": {
207+
Type: schema.TypeString,
208+
Computed: true,
209+
Description: "Member identifier.",
210+
},
211+
"ref": {
212+
Type: schema.TypeString,
213+
Computed: true,
214+
Description: "Member reference URL.",
215+
},
216+
"display_name": {
217+
Type: schema.TypeString,
218+
Computed: true,
219+
Description: "Member display name.",
220+
},
221+
}
222+
}

0 commit comments

Comments
 (0)