Skip to content

Commit 4d9bd08

Browse files
feat!: Implement Enterprise SCIM - Provision Groups & Users (#3852)
BREAKING CHANGE: `SCIMEnterpriseDisplayReference.Ref` is now of type `*string`.
1 parent f093aaa commit 4d9bd08

File tree

4 files changed

+281
-14
lines changed

4 files changed

+281
-14
lines changed

github/enterprise_scim.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,17 @@ const SCIMSchemasURINamespacesPatchOp = "urn:ietf:params:scim:api:messages:2.0:P
3232
type SCIMEnterpriseGroupAttributes struct {
3333
DisplayName *string `json:"displayName,omitempty"` // Human-readable name for a group.
3434
Members []*SCIMEnterpriseDisplayReference `json:"members,omitempty"` // List of members who are assigned to the group in SCIM provider
35-
ExternalID *string `json:"externalId,omitempty"` // This identifier is generated by a SCIM provider. Must be unique per user.
35+
ExternalID *string `json:"externalId,omitempty"` // This identifier is generated by a SCIM provider. Must be unique per group.
36+
Schemas []string `json:"schemas,omitempty"` // The URIs that are used to indicate the namespaces of the SCIM schemas.
3637
// Bellow: Only populated as a result of calling UpdateSCIMGroupAttribute:
37-
Schemas []string `json:"schemas,omitempty"` // The URIs that are used to indicate the namespaces of the SCIM schemas.
38-
ID *string `json:"id,omitempty"` // The internally generated id for the group object.
39-
Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the group.
38+
ID *string `json:"id,omitempty"` // The internally generated id for the group object.
39+
Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the group.
4040
}
4141

4242
// SCIMEnterpriseDisplayReference represents a JSON SCIM (System for Cross-domain Identity Management) resource reference.
4343
type SCIMEnterpriseDisplayReference struct {
4444
Value string `json:"value"` // The local unique identifier for the member (e.g., user ID or group ID).
45-
Ref string `json:"$ref"` // The URI reference to the member resource (e.g., https://api.github.com/scim/v2/Users/{id}).
45+
Ref *string `json:"$ref,omitempty"` // The URI reference to the Members or Groups resource (e.g., /scim/v2/enterprises/{enterprise}/Users/{scim_user_id}).
4646
Display *string `json:"display,omitempty"` // The display name associated with the member (e.g., user name or group name).
4747
}
4848

@@ -337,6 +337,50 @@ func (s *EnterpriseService) UpdateSCIMUserAttribute(ctx context.Context, enterpr
337337
return user, resp, nil
338338
}
339339

340+
// ProvisionSCIMGroup creates a SCIM group for an enterprise.
341+
//
342+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-group
343+
//
344+
//meta:operation POST /scim/v2/enterprises/{enterprise}/Groups
345+
func (s *EnterpriseService) ProvisionSCIMGroup(ctx context.Context, enterprise string, group SCIMEnterpriseGroupAttributes) (*SCIMEnterpriseGroupAttributes, *Response, error) {
346+
u := fmt.Sprintf("scim/v2/enterprises/%v/Groups", enterprise)
347+
req, err := s.client.NewRequest("POST", u, group)
348+
if err != nil {
349+
return nil, nil, err
350+
}
351+
req.Header.Set("Accept", mediaTypeSCIM)
352+
353+
groupProvisioned := new(SCIMEnterpriseGroupAttributes)
354+
resp, err := s.client.Do(ctx, req, groupProvisioned)
355+
if err != nil {
356+
return nil, resp, err
357+
}
358+
359+
return groupProvisioned, resp, nil
360+
}
361+
362+
// ProvisionSCIMUser creates an external identity for a new SCIM enterprise user.
363+
//
364+
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#provision-a-scim-enterprise-user
365+
//
366+
//meta:operation POST /scim/v2/enterprises/{enterprise}/Users
367+
func (s *EnterpriseService) ProvisionSCIMUser(ctx context.Context, enterprise string, user SCIMEnterpriseUserAttributes) (*SCIMEnterpriseUserAttributes, *Response, error) {
368+
u := fmt.Sprintf("scim/v2/enterprises/%v/Users", enterprise)
369+
req, err := s.client.NewRequest("POST", u, user)
370+
if err != nil {
371+
return nil, nil, err
372+
}
373+
req.Header.Set("Accept", mediaTypeSCIM)
374+
375+
userProvisioned := new(SCIMEnterpriseUserAttributes)
376+
resp, err := s.client.Do(ctx, req, userProvisioned)
377+
if err != nil {
378+
return nil, resp, err
379+
}
380+
381+
return userProvisioned, resp, nil
382+
}
383+
340384
// DeleteSCIMGroup deletes a SCIM group from an enterprise.
341385
//
342386
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#delete-a-scim-group-from-an-enterprise

github/enterprise_scim_test.go

Lines changed: 213 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestSCIMEnterpriseGroups_Marshal(t *testing.T) {
2626
DisplayName: Ptr("gn1"),
2727
Members: []*SCIMEnterpriseDisplayReference{{
2828
Value: "idm1",
29-
Ref: "https://api.github.com/scim/v2/enterprises/ee/Users/idm1",
29+
Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/idm1"),
3030
Display: Ptr("m1"),
3131
}},
3232
Schemas: []string{SCIMSchemasURINamespacesGroups},
@@ -94,7 +94,7 @@ func TestSCIMEnterpriseUsers_Marshal(t *testing.T) {
9494
UserName: "un1",
9595
Groups: []*SCIMEnterpriseDisplayReference{{
9696
Value: "idgn1",
97-
Ref: "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1",
97+
Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1"),
9898
Display: Ptr("gn1"),
9999
}},
100100
ID: Ptr("idun1"),
@@ -209,7 +209,7 @@ func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) {
209209
DisplayName: Ptr("dn"),
210210
Members: []*SCIMEnterpriseDisplayReference{{
211211
Value: "v",
212-
Ref: "r",
212+
Ref: Ptr("r"),
213213
Display: Ptr("d"),
214214
}},
215215
ExternalID: Ptr("eid"),
@@ -346,14 +346,14 @@ func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) {
346346
ExternalID: Ptr("de88"),
347347
Members: []*SCIMEnterpriseDisplayReference{{
348348
Value: "e7f9",
349-
Ref: "https://api.github.com/scim/v2/enterprises/ee/Users/e7f9",
349+
Ref: Ptr("https://api.github.com/scim/v2/enterprises/ee/Users/e7f9"),
350350
Display: Ptr("d1"),
351351
}},
352352
}},
353353
}
354354

355355
if diff := cmp.Diff(want, got); diff != "" {
356-
t.Errorf("Enterprise.ListProvisionedSCIMGroups diff mismatch (-want +got):\n%v", diff)
356+
t.Fatalf("Enterprise.ListProvisionedSCIMGroups diff mismatch (-want +got):\n%v", diff)
357357
}
358358

359359
const methodName = "ListProvisionedSCIMGroups"
@@ -461,7 +461,7 @@ func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) {
461461
}
462462

463463
if diff := cmp.Diff(want, got); diff != "" {
464-
t.Errorf("Enterprise.ListProvisionedSCIMUsers diff mismatch (-want +got):\n%v", diff)
464+
t.Fatalf("Enterprise.ListProvisionedSCIMUsers diff mismatch (-want +got):\n%v", diff)
465465
}
466466

467467
const methodName = "ListProvisionedSCIMUsers"
@@ -662,7 +662,7 @@ func TestEnterpriseService_UpdateSCIMGroupAttribute(t *testing.T) {
662662
DisplayName: Ptr("Employees"),
663663
Members: []*SCIMEnterpriseDisplayReference{{
664664
Value: "879d",
665-
Ref: "https://api.github.localhost/scim/v2/enterprises/ee/Users/879d",
665+
Ref: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/879d"),
666666
Display: Ptr("User 1"),
667667
}},
668668
Meta: &SCIMEnterpriseMeta{
@@ -687,7 +687,7 @@ func TestEnterpriseService_UpdateSCIMGroupAttribute(t *testing.T) {
687687
t.Fatalf("Enterprise.UpdateSCIMGroupAttribute returned unexpected error: %v", err)
688688
}
689689
if diff := cmp.Diff(want, got); diff != "" {
690-
t.Errorf("Enterprise.UpdateSCIMGroupAttribute diff mismatch (-want +got):\n%v", diff)
690+
t.Fatalf("Enterprise.UpdateSCIMGroupAttribute diff mismatch (-want +got):\n%v", diff)
691691
}
692692

693693
const methodName = "UpdateSCIMGroupAttribute"
@@ -792,7 +792,7 @@ func TestEnterpriseService_UpdateSCIMUserAttribute(t *testing.T) {
792792
t.Fatalf("Enterprise.UpdateSCIMUserAttribute returned unexpected error: %v", err)
793793
}
794794
if diff := cmp.Diff(want, got); diff != "" {
795-
t.Errorf("Enterprise.UpdateSCIMUserAttribute diff mismatch (-want +got):\n%v", diff)
795+
t.Fatalf("Enterprise.UpdateSCIMUserAttribute diff mismatch (-want +got):\n%v", diff)
796796
}
797797

798798
const methodName = "UpdateSCIMUserAttribute"
@@ -810,6 +810,210 @@ func TestEnterpriseService_UpdateSCIMUserAttribute(t *testing.T) {
810810
})
811811
}
812812

813+
func TestEnterpriseService_ProvisionSCIMGroup(t *testing.T) {
814+
t.Parallel()
815+
client, mux, _ := setup(t)
816+
817+
mux.HandleFunc("/scim/v2/enterprises/ee/Groups", func(w http.ResponseWriter, r *http.Request) {
818+
testMethod(t, r, "POST")
819+
testHeader(t, r, "Accept", mediaTypeSCIM)
820+
testBody(t, r, `{"displayName":"dn","members":[{"value":"879d","display":"d1"},{"value":"0db5","display":"d2"}],"externalId":"8aa1","schemas":["`+SCIMSchemasURINamespacesGroups+`"]}`+"\n")
821+
w.WriteHeader(http.StatusCreated)
822+
fmt.Fprint(w, `{
823+
"schemas": ["`+SCIMSchemasURINamespacesGroups+`"],
824+
"id": "abcd",
825+
"externalId": "8aa1",
826+
"displayName": "dn",
827+
"members": [
828+
{
829+
"value": "879d",
830+
"$ref": "https://api.github.localhost/scim/v2/enterprises/ee/Users/879d",
831+
"display": "d1"
832+
},
833+
{
834+
"value": "0db5",
835+
"$ref": "https://api.github.localhost/scim/v2/enterprises/ee/Users/0db5",
836+
"display": "d2"
837+
}
838+
],
839+
"meta": {
840+
"resourceType": "Group",
841+
"created": `+referenceTimeStr+`,
842+
"lastModified": `+referenceTimeStr+`,
843+
"location": "https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd"
844+
}
845+
}`)
846+
})
847+
want := &SCIMEnterpriseGroupAttributes{
848+
Schemas: []string{SCIMSchemasURINamespacesGroups},
849+
ID: Ptr("abcd"),
850+
ExternalID: Ptr("8aa1"),
851+
DisplayName: Ptr("dn"),
852+
Members: []*SCIMEnterpriseDisplayReference{{
853+
Value: "879d",
854+
Ref: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/879d"),
855+
Display: Ptr("d1"),
856+
}, {
857+
Value: "0db5",
858+
Ref: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/0db5"),
859+
Display: Ptr("d2"),
860+
}},
861+
Meta: &SCIMEnterpriseMeta{
862+
ResourceType: "Group",
863+
Created: &Timestamp{referenceTime},
864+
LastModified: &Timestamp{referenceTime},
865+
Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Groups/abcd"),
866+
},
867+
}
868+
869+
ctx := t.Context()
870+
input := SCIMEnterpriseGroupAttributes{
871+
Schemas: []string{SCIMSchemasURINamespacesGroups},
872+
ExternalID: Ptr("8aa1"),
873+
DisplayName: Ptr("dn"),
874+
Members: []*SCIMEnterpriseDisplayReference{{
875+
Value: "879d",
876+
Display: Ptr("d1"),
877+
}, {
878+
Value: "0db5",
879+
Display: Ptr("d2"),
880+
}},
881+
}
882+
got, _, err := client.Enterprise.ProvisionSCIMGroup(ctx, "ee", input)
883+
if err != nil {
884+
t.Fatalf("Enterprise.ProvisionSCIMGroup returned unexpected error: %v", err)
885+
}
886+
if diff := cmp.Diff(want, got); diff != "" {
887+
t.Fatalf("Enterprise.ProvisionSCIMGroup diff mismatch (-want +got):\n%v", diff)
888+
}
889+
890+
const methodName = "ProvisionSCIMGroup"
891+
testBadOptions(t, methodName, func() (err error) {
892+
_, _, err = client.Enterprise.ProvisionSCIMGroup(ctx, "\n", SCIMEnterpriseGroupAttributes{})
893+
return err
894+
})
895+
896+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
897+
got, resp, err := client.Enterprise.ProvisionSCIMGroup(ctx, "ee", input)
898+
if got != nil {
899+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
900+
}
901+
return resp, err
902+
})
903+
}
904+
905+
func TestEnterpriseService_ProvisionSCIMUser(t *testing.T) {
906+
t.Parallel()
907+
client, mux, _ := setup(t)
908+
909+
mux.HandleFunc("/scim/v2/enterprises/ee/Users", func(w http.ResponseWriter, r *http.Request) {
910+
testMethod(t, r, "POST")
911+
testHeader(t, r, "Accept", mediaTypeSCIM)
912+
testBody(t, r, `{"displayName":"DOE John","name":{"givenName":"John","familyName":"Doe","formatted":"John Doe"},"userName":"e123","emails":[{"value":"john@email.com","primary":true,"type":"work"}],"roles":[{"value":"User","primary":false}],"externalId":"e123","active":true,"schemas":["`+SCIMSchemasURINamespacesUser+`"]}`+"\n")
913+
w.WriteHeader(http.StatusCreated)
914+
fmt.Fprint(w, `{
915+
"schemas": ["`+SCIMSchemasURINamespacesUser+`"],
916+
"id": "7fce",
917+
"externalId": "e123",
918+
"active": true,
919+
"userName": "e123",
920+
"name": {
921+
"formatted": "John Doe",
922+
"familyName": "Doe",
923+
"givenName": "John"
924+
},
925+
"displayName": "DOE John",
926+
"emails": [{
927+
"value": "john@email.com",
928+
"type": "work",
929+
"primary": true
930+
}],
931+
"roles": [{
932+
"value": "User",
933+
"primary": false
934+
}],
935+
"meta": {
936+
"resourceType": "User",
937+
"created": `+referenceTimeStr+`,
938+
"lastModified": `+referenceTimeStr+`,
939+
"location": "https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce"
940+
}
941+
}`)
942+
})
943+
want := &SCIMEnterpriseUserAttributes{
944+
Schemas: []string{SCIMSchemasURINamespacesUser},
945+
ID: Ptr("7fce"),
946+
ExternalID: "e123",
947+
Active: true,
948+
UserName: "e123",
949+
DisplayName: "DOE John",
950+
Name: &SCIMEnterpriseUserName{
951+
Formatted: Ptr("John Doe"),
952+
FamilyName: "Doe",
953+
GivenName: "John",
954+
},
955+
Emails: []*SCIMEnterpriseUserEmail{{
956+
Value: "john@email.com",
957+
Type: "work",
958+
Primary: true,
959+
}},
960+
Roles: []*SCIMEnterpriseUserRole{{
961+
Value: "User",
962+
Primary: Ptr(false),
963+
}},
964+
Meta: &SCIMEnterpriseMeta{
965+
ResourceType: "User",
966+
Created: &Timestamp{referenceTime},
967+
LastModified: &Timestamp{referenceTime},
968+
Location: Ptr("https://api.github.localhost/scim/v2/enterprises/ee/Users/7fce"),
969+
},
970+
}
971+
972+
ctx := t.Context()
973+
input := SCIMEnterpriseUserAttributes{
974+
Schemas: []string{SCIMSchemasURINamespacesUser},
975+
ExternalID: "e123",
976+
Active: true,
977+
UserName: "e123",
978+
Name: &SCIMEnterpriseUserName{
979+
Formatted: Ptr("John Doe"),
980+
FamilyName: "Doe",
981+
GivenName: "John",
982+
},
983+
DisplayName: "DOE John",
984+
Emails: []*SCIMEnterpriseUserEmail{{
985+
Value: "john@email.com",
986+
Type: "work",
987+
Primary: true,
988+
}},
989+
Roles: []*SCIMEnterpriseUserRole{{
990+
Value: "User",
991+
Primary: Ptr(false),
992+
}},
993+
}
994+
got, _, err := client.Enterprise.ProvisionSCIMUser(ctx, "ee", input)
995+
if err != nil {
996+
t.Fatalf("Enterprise.ProvisionSCIMUser returned unexpected error: %v", err)
997+
}
998+
if diff := cmp.Diff(want, got); diff != "" {
999+
t.Fatalf("Enterprise.ProvisionSCIMUser diff mismatch (-want +got):\n%v", diff)
1000+
}
1001+
1002+
const methodName = "ProvisionSCIMUser"
1003+
testBadOptions(t, methodName, func() (err error) {
1004+
_, _, err = client.Enterprise.ProvisionSCIMUser(ctx, "\n", SCIMEnterpriseUserAttributes{})
1005+
return err
1006+
})
1007+
1008+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
1009+
got, resp, err := client.Enterprise.ProvisionSCIMUser(ctx, "ee", input)
1010+
if got != nil {
1011+
t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
1012+
}
1013+
return resp, err
1014+
})
1015+
}
1016+
8131017
func TestEnterpriseService_DeleteSCIMGroup(t *testing.T) {
8141018
t.Parallel()
8151019
client, mux, _ := setup(t)

github/github-accessors.go

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

github/github-accessors_test.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)