Skip to content

Commit e53f61e

Browse files
feat!: Implement Enterprise SCIM - EnterpriseService.ListProvisionedSCIMGroups (#3814)
BREAKING CHANGE: `SCIMService.ListSCIMProvisionedGroupsForEnterprise` is now `EnterpriseService.ListProvisionedSCIMEnterpriseGroups`.
1 parent d69610a commit e53f61e

File tree

6 files changed

+432
-390
lines changed

6 files changed

+432
-390
lines changed

github/enterprise_scim.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2025 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"context"
10+
"fmt"
11+
)
12+
13+
// SCIMSchemasURINamespacesGroups is the SCIM schema URI namespace for group resources.
14+
// This constant represents the standard SCIM core schema for group objects as defined by RFC 7643.
15+
const SCIMSchemasURINamespacesGroups = "urn:ietf:params:scim:schemas:core:2.0:Group"
16+
17+
// SCIMSchemasURINamespacesListResponse is the SCIM schema URI namespace for list response resources.
18+
// This constant represents the standard SCIM namespace for list responses used in paginated queries, as defined by RFC 7644.
19+
const SCIMSchemasURINamespacesListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
20+
21+
// SCIMEnterpriseGroupAttributes represents supported SCIM Enterprise group attributes.
22+
//
23+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#supported-scim-group-attributes
24+
type SCIMEnterpriseGroupAttributes struct {
25+
DisplayName *string `json:"displayName,omitempty"` // Human-readable name for a group.
26+
Members []*SCIMEnterpriseDisplayReference `json:"members,omitempty"` // List of members who are assigned to the group in SCIM provider
27+
ExternalID *string `json:"externalId,omitempty"` // This identifier is generated by a SCIM provider. Must be unique per user.
28+
// Bellow: Only populated as a result of calling SetSCIMInformationForProvisionedGroup:
29+
Schemas []string `json:"schemas,omitempty"` // The URIs that are used to indicate the namespaces of the SCIM schemas.
30+
ID *string `json:"id,omitempty"` // The internally generated id for the group object.
31+
Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the group.
32+
}
33+
34+
// SCIMEnterpriseDisplayReference represents a JSON SCIM (System for Cross-domain Identity Management) resource reference.
35+
type SCIMEnterpriseDisplayReference struct {
36+
Value string `json:"value"` // The local unique identifier for the member (e.g., user ID or group ID).
37+
Ref string `json:"$ref"` // The URI reference to the member resource (e.g., https://api.github.com/scim/v2/Users/{id}).
38+
Display *string `json:"display,omitempty"` // The display name associated with the member (e.g., user name or group name).
39+
}
40+
41+
// SCIMEnterpriseMeta represents metadata about the SCIM resource.
42+
type SCIMEnterpriseMeta struct {
43+
ResourceType string `json:"resourceType"` // A type of a resource (`User` or `Group`).
44+
Created *Timestamp `json:"created,omitempty"` // A date and time when the user was created.
45+
LastModified *Timestamp `json:"lastModified,omitempty"` // A date and time when the user was last modified.
46+
Location *string `json:"location,omitempty"` // A URL location of an object
47+
}
48+
49+
// SCIMEnterpriseGroups represents the result of calling ListProvisionedSCIMGroups.
50+
type SCIMEnterpriseGroups struct {
51+
Schemas []string `json:"schemas,omitempty"`
52+
TotalResults *int `json:"totalResults,omitempty"`
53+
Resources []*SCIMEnterpriseGroupAttributes `json:"Resources,omitempty"`
54+
StartIndex *int `json:"startIndex,omitempty"`
55+
ItemsPerPage *int `json:"itemsPerPage,omitempty"`
56+
}
57+
58+
// ListProvisionedSCIMGroupsEnterpriseOptions represents query parameters for ListProvisionedSCIMGroups.
59+
//
60+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-provisioned-scim-groups-for-an-enterprise--parameters
61+
type ListProvisionedSCIMGroupsEnterpriseOptions struct {
62+
// If specified, only results that match the specified filter will be returned.
63+
// Possible filters are `externalId`, `id`, and `displayName`. For example, `externalId eq "a123"`.
64+
Filter string `url:"filter,omitempty"`
65+
// Excludes the specified attribute from being returned in the results.
66+
ExcludedAttributes string `url:"excludedAttributes,omitempty"`
67+
// Used for pagination: the starting index of the first result to return when paginating through values.
68+
// Default: 1.
69+
StartIndex int `url:"startIndex,omitempty"`
70+
// Used for pagination: the number of results to return per page.
71+
// Default: 30.
72+
Count int `url:"count,omitempty"`
73+
}
74+
75+
// ListProvisionedSCIMGroups lists provisioned SCIM groups in an enterprise.
76+
//
77+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-provisioned-scim-groups-for-an-enterprise
78+
//
79+
//meta:operation GET /scim/v2/enterprises/{enterprise}/Groups
80+
func (s *EnterpriseService) ListProvisionedSCIMGroups(ctx context.Context, enterprise string, opts *ListProvisionedSCIMGroupsEnterpriseOptions) (*SCIMEnterpriseGroups, *Response, error) {
81+
u := fmt.Sprintf("scim/v2/enterprises/%v/Groups", enterprise)
82+
u, err := addOptions(u, opts)
83+
if err != nil {
84+
return nil, nil, err
85+
}
86+
87+
req, err := s.client.NewRequest("GET", u, nil)
88+
if err != nil {
89+
return nil, nil, err
90+
}
91+
92+
groups := new(SCIMEnterpriseGroups)
93+
resp, err := s.client.Do(ctx, req, groups)
94+
if err != nil {
95+
return nil, resp, err
96+
}
97+
98+
return groups, resp, nil
99+
}

github/enterprise_scim_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright 2025 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
package github
7+
8+
import (
9+
"net/http"
10+
"testing"
11+
12+
"github.com/google/go-cmp/cmp"
13+
)
14+
15+
func TestSCIMEnterpriseGroups_Marshal(t *testing.T) {
16+
t.Parallel()
17+
testJSONMarshal(t, &SCIMEnterpriseGroups{}, "{}")
18+
19+
u := &SCIMEnterpriseGroups{
20+
Schemas: []string{SCIMSchemasURINamespacesListResponse},
21+
TotalResults: Ptr(1),
22+
ItemsPerPage: Ptr(1),
23+
StartIndex: Ptr(1),
24+
Resources: []*SCIMEnterpriseGroupAttributes{{
25+
DisplayName: Ptr("gn1"),
26+
Members: []*SCIMEnterpriseDisplayReference{{
27+
Value: "idm1",
28+
Ref: "https://api.github.com/scim/v2/enterprises/ee/Users/idm1",
29+
Display: Ptr("m1"),
30+
}},
31+
Schemas: []string{SCIMSchemasURINamespacesGroups},
32+
ExternalID: Ptr("eidgn1"),
33+
ID: Ptr("idgn1"),
34+
Meta: &SCIMEnterpriseMeta{
35+
ResourceType: "Group",
36+
Created: &Timestamp{referenceTime},
37+
LastModified: &Timestamp{referenceTime},
38+
Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1"),
39+
},
40+
}},
41+
}
42+
43+
want := `{
44+
"schemas": ["` + SCIMSchemasURINamespacesListResponse + `"],
45+
"totalResults": 1,
46+
"itemsPerPage": 1,
47+
"startIndex": 1,
48+
"Resources": [{
49+
"schemas": ["` + SCIMSchemasURINamespacesGroups + `"],
50+
"id": "idgn1",
51+
"externalId": "eidgn1",
52+
"displayName": "gn1",
53+
"meta": {
54+
"resourceType": "Group",
55+
"created": ` + referenceTimeStr + `,
56+
"lastModified": ` + referenceTimeStr + `,
57+
"location": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1"
58+
},
59+
"members": [{
60+
"value": "idm1",
61+
"$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/idm1",
62+
"display": "m1"
63+
}]
64+
}]
65+
}`
66+
67+
testJSONMarshal(t, u, want)
68+
}
69+
70+
func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) {
71+
t.Parallel()
72+
testJSONMarshal(t, &SCIMEnterpriseGroupAttributes{}, "{}")
73+
74+
u := &SCIMEnterpriseGroupAttributes{
75+
DisplayName: Ptr("dn"),
76+
Members: []*SCIMEnterpriseDisplayReference{{
77+
Value: "v",
78+
Ref: "r",
79+
Display: Ptr("d"),
80+
}},
81+
ExternalID: Ptr("eid"),
82+
ID: Ptr("id"),
83+
Schemas: []string{"s1"},
84+
Meta: &SCIMEnterpriseMeta{
85+
ResourceType: "rt",
86+
Created: &Timestamp{referenceTime},
87+
LastModified: &Timestamp{referenceTime},
88+
Location: Ptr("l"),
89+
},
90+
}
91+
92+
want := `{
93+
"schemas": ["s1"],
94+
"externalId": "eid",
95+
"displayName": "dn",
96+
"members" : [{
97+
"value": "v",
98+
"$ref": "r",
99+
"display": "d"
100+
}],
101+
"id": "id",
102+
"meta": {
103+
"resourceType": "rt",
104+
"created": ` + referenceTimeStr + `,
105+
"lastModified": ` + referenceTimeStr + `,
106+
"location": "l"
107+
}
108+
}`
109+
110+
testJSONMarshal(t, u, want)
111+
}
112+
113+
func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) {
114+
t.Parallel()
115+
client, mux, _ := setup(t)
116+
117+
mux.HandleFunc("/scim/v2/enterprises/ee/Groups", func(w http.ResponseWriter, r *http.Request) {
118+
testMethod(t, r, "GET")
119+
testFormValues(t, r, values{
120+
"startIndex": "1",
121+
"excludedAttributes": "members,meta",
122+
"count": "3",
123+
"filter": `externalId eq "914a"`,
124+
})
125+
w.WriteHeader(http.StatusOK)
126+
_, _ = w.Write([]byte(`{
127+
"schemas": ["` + SCIMSchemasURINamespacesListResponse + `"],
128+
"totalResults": 1,
129+
"itemsPerPage": 1,
130+
"startIndex": 1,
131+
"Resources": [{
132+
"schemas": ["` + SCIMSchemasURINamespacesGroups + `"],
133+
"id": "914a",
134+
"externalId": "de88",
135+
"displayName": "gn1",
136+
"meta": {
137+
"resourceType": "Group",
138+
"created": ` + referenceTimeStr + `,
139+
"lastModified": ` + referenceTimeStr + `,
140+
"location": "https://api.github.com/scim/v2/enterprises/ee/Groups/914a"
141+
},
142+
"members": [{
143+
"value": "e7f9",
144+
"$ref": "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9",
145+
"display": "d1"
146+
}]
147+
}]
148+
}`))
149+
})
150+
151+
ctx := t.Context()
152+
opts := &ListProvisionedSCIMGroupsEnterpriseOptions{
153+
StartIndex: 1,
154+
ExcludedAttributes: "members,meta",
155+
Count: 3,
156+
Filter: `externalId eq "914a"`,
157+
}
158+
groups, _, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "ee", opts)
159+
if err != nil {
160+
t.Errorf("Enterprise.ListProvisionedSCIMGroups returned error: %v", err)
161+
}
162+
163+
want := SCIMEnterpriseGroups{
164+
Schemas: []string{SCIMSchemasURINamespacesListResponse},
165+
TotalResults: Ptr(1),
166+
ItemsPerPage: Ptr(1),
167+
StartIndex: Ptr(1),
168+
Resources: []*SCIMEnterpriseGroupAttributes{{
169+
ID: Ptr("914a"),
170+
Meta: &SCIMEnterpriseMeta{
171+
ResourceType: "Group",
172+
Created: &Timestamp{referenceTime},
173+
LastModified: &Timestamp{referenceTime},
174+
Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/Groups/914a"),
175+
},
176+
DisplayName: Ptr("gn1"),
177+
Schemas: []string{SCIMSchemasURINamespacesGroups},
178+
ExternalID: Ptr("de88"),
179+
Members: []*SCIMEnterpriseDisplayReference{{
180+
Value: "e7f9",
181+
Ref: "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9",
182+
Display: Ptr("d1"),
183+
}},
184+
}},
185+
}
186+
187+
if diff := cmp.Diff(want, *groups); diff != "" {
188+
t.Errorf("Enterprise.ListProvisionedSCIMGroups diff mismatch (-want +got):\n%v", diff)
189+
}
190+
191+
const methodName = "ListProvisionedSCIMGroups"
192+
testBadOptions(t, methodName, func() (err error) {
193+
_, _, err = client.Enterprise.ListProvisionedSCIMGroups(ctx, "\n", opts)
194+
return err
195+
})
196+
197+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
198+
_, r, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "o", opts)
199+
return r, err
200+
})
201+
}

0 commit comments

Comments
 (0)