Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions github/enterprise_scim.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import (
// This constant represents the standard SCIM core schema for group objects as defined by RFC 7643.
const SCIMSchemasURINamespacesGroups = "urn:ietf:params:scim:schemas:core:2.0:Group"

// SCIMSchemasURINamespacesUser is the SCIM schema URI namespace for user resources.
// This constant represents the standard SCIM core schema for user objects as defined by RFC 7643.
const SCIMSchemasURINamespacesUser = "urn:ietf:params:scim:schemas:core:2.0:User"

// SCIMSchemasURINamespacesListResponse is the SCIM schema URI namespace for list response resources.
// This constant represents the standard SCIM namespace for list responses used in paginated queries, as defined by RFC 7644.
const SCIMSchemasURINamespacesListResponse = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
Expand Down Expand Up @@ -72,6 +76,70 @@ type ListProvisionedSCIMGroupsEnterpriseOptions struct {
Count int `url:"count,omitempty"`
}

// SCIMEnterpriseUserAttributes represents supported SCIM enterprise user attributes.
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#supported-scim-user-attributes
type SCIMEnterpriseUserAttributes struct {
DisplayName string `json:"displayName"` // Human-readable name for a user
Name *SCIMEnterpriseUserName `json:"name,omitempty"` // The user's full name
UserName string `json:"userName"` // The username for the user (GitHub Account after normalized), generated by the SCIM provider. Must be unique per user.
Emails []*SCIMEnterpriseUserEmail `json:"emails"` // List of the user's emails. They all must be unique per user.
Roles []*SCIMEnterpriseUserRole `json:"roles,omitempty"` // List of the user's roles.
ExternalID string `json:"externalId"` // This identifier is generated by a SCIM provider. Must be unique per user.
Active bool `json:"active"` // Indicates whether the identity is active (true) or should be suspended (false).
Schemas []string `json:"schemas"` // The URIs that are used to indicate the namespaces of the SCIM schemas.
// Bellow: Only populated as a result of calling SetSCIMInformationForProvisionedUser:
ID *string `json:"id,omitempty"` // Identifier generated by the GitHub's SCIM endpoint.
Groups []*SCIMEnterpriseDisplayReference `json:"groups,omitempty"` // List of groups who are assigned to the user in SCIM provider
Meta *SCIMEnterpriseMeta `json:"meta,omitempty"` // The metadata associated with the creation/updates to the user.
}

// SCIMEnterpriseUserName represents SCIM enterprise user's name information.
type SCIMEnterpriseUserName struct {
GivenName string `json:"givenName"` // The first name of the user.
FamilyName string `json:"familyName"` // The last name of the user.
Formatted *string `json:"formatted,omitempty"` // The user's full name, including all middle names, titles, and suffixes, formatted for display.
MiddleName *string `json:"middleName,omitempty"` // The middle name(s) of the user.
}

// SCIMEnterpriseUserEmail represents SCIM enterprise user's emails.
type SCIMEnterpriseUserEmail struct {
Value string `json:"value"` // The email address.
Primary bool `json:"primary"` // Whether this email address is the primary address.
Type string `json:"type"` // The type of email address
}

// SCIMEnterpriseUserRole is an enterprise-wide role granted to the user.
type SCIMEnterpriseUserRole struct {
Value string `json:"value"` // The role value representing a user role in GitHub.
Display *string `json:"display,omitempty"`
Type *string `json:"type,omitempty"`
Primary *bool `json:"primary,omitempty"` // Is the role a primary role for the user?
}

// SCIMEnterpriseUsers represents the result of calling ProvisionSCIMEnterpriseUser.
type SCIMEnterpriseUsers struct {
Schemas []string `json:"schemas,omitempty"`
TotalResults *int `json:"totalResults,omitempty"`
ItemsPerPage *int `json:"itemsPerPage,omitempty"`
StartIndex *int `json:"startIndex,omitempty"`
Resources []*SCIMEnterpriseUserAttributes `json:"Resources,omitempty"`
}

// ListProvisionedSCIMUsersEnterpriseOptions represents query parameters for ListSCIMProvisionedUsers.
//
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-scim-provisioned-identities-for-an-enterprise
type ListProvisionedSCIMUsersEnterpriseOptions struct {
// If specified, only results that match the specified filter will be returned.
// Possible filters are `userName`, `externalId`, `id`, and `displayName`. For example, `externalId eq "a123"`.
Filter string `url:"filter,omitempty"`
// Used for pagination: the starting index of the first result to return when paginating through values.
// Default: 1.
StartIndex int `url:"startIndex,omitempty"`
// Used for pagination: the number of results to return per page.
// Default: 30.
Count int `url:"count,omitempty"`
Comment on lines +134 to +140
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These omitempty fields should be pointers since they are optional values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the previous PR, #discussion_r2524023305; @alexandear recommended this structure for options structures:

go-github/github/github.go

Lines 262 to 288 in 4ed75ac

type ListCursorOptions struct {
// For paginated result sets, page of results to retrieve.
Page string `url:"page,omitempty"`
// For paginated result sets, the number of results to include per page.
PerPage int `url:"per_page,omitempty"`
// For paginated result sets, the number of results per page (max 100), starting from the first matching result.
// This parameter must not be used in combination with last.
First int `url:"first,omitempty"`
// For paginated result sets, the number of results per page (max 100), starting from the last matching result.
// This parameter must not be used in combination with first.
Last int `url:"last,omitempty"`
// A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
After string `url:"after,omitempty"`
// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
Before string `url:"before,omitempty"`
// A cursor, as given in the Link header. If specified, the query continues the search using this cursor.
Cursor string `url:"cursor,omitempty"`
}
// UploadOptions specifies the parameters to methods that support uploads.
type UploadOptions struct {

}

// ListProvisionedSCIMGroups lists provisioned SCIM groups in an enterprise.
//
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-provisioned-scim-groups-for-an-enterprise
Expand All @@ -88,6 +156,7 @@ func (s *EnterpriseService) ListProvisionedSCIMGroups(ctx context.Context, enter
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaTypeSCIM)

groups := new(SCIMEnterpriseGroups)
resp, err := s.client.Do(ctx, req, groups)
Expand All @@ -97,3 +166,30 @@ func (s *EnterpriseService) ListProvisionedSCIMGroups(ctx context.Context, enter

return groups, resp, nil
}

// ListProvisionedSCIMUsers lists provisioned SCIM enterprise users.
//
// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/scim#list-scim-provisioned-identities-for-an-enterprise
//
//meta:operation GET /scim/v2/enterprises/{enterprise}/Users
func (s *EnterpriseService) ListProvisionedSCIMUsers(ctx context.Context, enterprise string, opts *ListProvisionedSCIMUsersEnterpriseOptions) (*SCIMEnterpriseUsers, *Response, error) {
u := fmt.Sprintf("scim/v2/enterprises/%v/Users", enterprise)
u, err := addOptions(u, opts)
if err != nil {
return nil, nil, err
}

req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
req.Header.Set("Accept", mediaTypeSCIM)

users := new(SCIMEnterpriseUsers)
resp, err := s.client.Do(ctx, req, users)
if err != nil {
return nil, resp, err
}

return users, resp, nil
}
243 changes: 241 additions & 2 deletions github/enterprise_scim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,139 @@ func TestSCIMEnterpriseGroups_Marshal(t *testing.T) {
testJSONMarshal(t, u, want)
}

func TestSCIMEnterpriseUsers_Marshal(t *testing.T) {
t.Parallel()
testJSONMarshal(t, &SCIMEnterpriseUsers{}, "{}")

u := &SCIMEnterpriseUsers{
Schemas: []string{SCIMSchemasURINamespacesListResponse},
TotalResults: Ptr(1),
ItemsPerPage: Ptr(1),
StartIndex: Ptr(1),
Resources: []*SCIMEnterpriseUserAttributes{{
Active: true,
Emails: []*SCIMEnterpriseUserEmail{{
Primary: true,
Type: "work",
Value: "[email protected]",
}},
Roles: []*SCIMEnterpriseUserRole{{
Display: Ptr("rd1"),
Primary: Ptr(true),
Type: Ptr("rt1"),
Value: "rv1",
}},
Schemas: []string{SCIMSchemasURINamespacesUser},
UserName: "un1",
Groups: []*SCIMEnterpriseDisplayReference{{
Value: "idgn1",
Ref: "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1",
Display: Ptr("gn1"),
}},
ID: Ptr("idun1"),
ExternalID: "eidun1",
DisplayName: "dun1",
Meta: &SCIMEnterpriseMeta{
ResourceType: "User",
Created: &Timestamp{referenceTime},
LastModified: &Timestamp{referenceTime},
Location: Ptr("https://api.github.com/scim/v2/enterprises/ee/User/idun1"),
},
Name: &SCIMEnterpriseUserName{
GivenName: "gnn1",
FamilyName: "fnn1",
Formatted: Ptr("f1"),
MiddleName: Ptr("mn1"),
},
}},
}

want := `{
"schemas": ["` + SCIMSchemasURINamespacesListResponse + `"],
"TotalResults": 1,
"itemsPerPage": 1,
"StartIndex": 1,
"Resources": [{
"active": true,
"emails": [{
"primary": true,
"type": "work",
"value": "[email protected]"
}],
"roles": [{
"display": "rd1",
"primary": true,
"type": "rt1",
"value": "rv1"
}],
"schemas": ["` + SCIMSchemasURINamespacesUser + `"],
"userName": "un1",
"groups": [{
"value": "idgn1",
"$ref": "https://api.github.com/scim/v2/enterprises/ee/Groups/idgn1",
"display": "gn1"
}],
"id": "idun1",
"externalId": "eidun1",
"name": {
"givenName": "gnn1",
"familyName": "fnn1",
"formatted": "f1",
"middleName": "mn1"
},
"displayName": "dun1",
"meta": {
"resourceType": "User",
"created": ` + referenceTimeStr + `,
"lastModified": ` + referenceTimeStr + `,
"location": "https://api.github.com/scim/v2/enterprises/ee/User/idun1"
}
}]
}`

testJSONMarshal(t, u, want)
}

func TestListProvisionedSCIMGroupsEnterpriseOptions_Marshal(t *testing.T) {
t.Parallel()
testJSONMarshal(t, &ListProvisionedSCIMGroupsEnterpriseOptions{}, "{}")

u := &ListProvisionedSCIMGroupsEnterpriseOptions{
Filter: "f",
ExcludedAttributes: "ea",
StartIndex: 5,
Count: 9,
}

want := `{
"filter": "f",
"excludedAttributes": "ea",
"startIndex": 5,
"count": 9
}`

testJSONMarshal(t, u, want)
}

func TestListProvisionedSCIMUsersEnterpriseOptions_Marshal(t *testing.T) {
t.Parallel()
testJSONMarshal(t, &ListProvisionedSCIMUsersEnterpriseOptions{}, "{}")

u := &ListProvisionedSCIMUsersEnterpriseOptions{
Filter: "f",
StartIndex: 3,
Count: 7,
}

want := `{
"filter": "f",
"startIndex": 3,
"count": 7
}`

testJSONMarshal(t, u, want)
}

func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) {
t.Parallel()
testJSONMarshal(t, &SCIMEnterpriseGroupAttributes{}, "{}")
Expand Down Expand Up @@ -110,12 +243,13 @@ func TestSCIMEnterpriseGroupAttributes_Marshal(t *testing.T) {
testJSONMarshal(t, u, want)
}

func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) {
func TestEnterpriseService_ListProvisionedSCIMGroups(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/scim/v2/enterprises/ee/Groups", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeSCIM)
testFormValues(t, r, values{
"startIndex": "1",
"excludedAttributes": "members,meta",
Expand Down Expand Up @@ -157,7 +291,7 @@ func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) {
}
groups, _, err := client.Enterprise.ListProvisionedSCIMGroups(ctx, "ee", opts)
if err != nil {
t.Errorf("Enterprise.ListProvisionedSCIMGroups returned error: %v", err)
t.Fatalf("Enterprise.ListProvisionedSCIMGroups returned unexpected error: %v", err)
}

want := SCIMEnterpriseGroups{
Expand Down Expand Up @@ -199,3 +333,108 @@ func TestEnterpriseService_ListProvisionedSCIMEnterpriseGroups(t *testing.T) {
return r, err
})
}

func TestEnterpriseService_ListProvisionedSCIMUsers(t *testing.T) {
t.Parallel()
client, mux, _ := setup(t)

mux.HandleFunc("/scim/v2/enterprises/octo-org/Users", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeSCIM)
testFormValues(t, r, values{
"startIndex": "1",
"count": "3",
"filter": `userName eq "[email protected]"`,
})
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{
"schemas": ["` + SCIMSchemasURINamespacesListResponse + `"],
"totalResults": 1,
"itemsPerPage": 1,
"startIndex": 1,
"Resources": [
{
"schemas": ["` + SCIMSchemasURINamespacesUser + `"],
"id": "5fc0",
"externalId": "00u1",
"userName": "[email protected]",
"displayName": "Mona Octocat",
"name": {
"givenName": "Mona",
"familyName": "Octocat",
"formatted": "Mona Octocat"
},
"emails": [
{
"value": "[email protected]",
"primary": true
}
],
"active": true,
"meta": {
"resourceType": "User",
"created": ` + referenceTimeStr + `,
"lastModified": ` + referenceTimeStr + `,
"location": "https://api.github.com/scim/v2/organizations/octo-org/Users/5fc0"
}
}
]
}`))
})

ctx := t.Context()
opts := &ListProvisionedSCIMUsersEnterpriseOptions{
StartIndex: 1,
Count: 3,
Filter: `userName eq "[email protected]"`,
}
users, _, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "octo-org", opts)
if err != nil {
t.Fatalf("Enterprise.ListProvisionedSCIMUsers returned unexpected error: %v", err)
}

want := SCIMEnterpriseUsers{
Schemas: []string{SCIMSchemasURINamespacesListResponse},
TotalResults: Ptr(1),
ItemsPerPage: Ptr(1),
StartIndex: Ptr(1),
Resources: []*SCIMEnterpriseUserAttributes{{
Schemas: []string{SCIMSchemasURINamespacesUser},
ID: Ptr("5fc0"),
ExternalID: "00u1",
UserName: "[email protected]",
DisplayName: "Mona Octocat",
Name: &SCIMEnterpriseUserName{
GivenName: "Mona",
FamilyName: "Octocat",
Formatted: Ptr("Mona Octocat"),
},
Emails: []*SCIMEnterpriseUserEmail{{
Value: "[email protected]",
Primary: true,
}},
Active: true,
Meta: &SCIMEnterpriseMeta{
ResourceType: "User",
Created: &Timestamp{referenceTime},
LastModified: &Timestamp{referenceTime},
Location: Ptr("https://api.github.com/scim/v2/organizations/octo-org/Users/5fc0"),
},
}},
}

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

const methodName = "ListProvisionedSCIMUsers"
testBadOptions(t, methodName, func() (err error) {
_, _, err = client.Enterprise.ListProvisionedSCIMUsers(ctx, "\n", opts)
return err
})

testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
_, r, err := client.Enterprise.ListProvisionedSCIMUsers(ctx, "o", opts)
return r, err
})
}
Loading