From d4b4192f3eef0d91a377a1950213a662d2cce3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Penteado?= <4219131+joaopenteado@users.noreply.github.com> Date: Fri, 21 Jul 2023 01:27:39 +0900 Subject: [PATCH 1/4] Added support for SAML SSO authorization APIs --- github/github-accessors.go | 88 ++++++++++++++ github/github-accessors_test.go | 110 ++++++++++++++++++ github/orgs_credential_authorizations.go | 90 ++++++++++++++ github/orgs_credential_authorizations_test.go | 92 +++++++++++++++ 4 files changed, 380 insertions(+) create mode 100644 github/orgs_credential_authorizations.go create mode 100644 github/orgs_credential_authorizations_test.go diff --git a/github/github-accessors.go b/github/github-accessors.go index 35e61043ab8..4dd5529a3a3 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -4670,6 +4670,94 @@ func (c *CreateUserProjectOptions) GetBody() string { return *c.Body } +// GetAuthorizedCredentialExpiresAt returns the AuthorizedCredentialExpiresAt field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetAuthorizedCredentialExpiresAt() Timestamp { + if c == nil || c.AuthorizedCredentialExpiresAt == nil { + return Timestamp{} + } + return *c.AuthorizedCredentialExpiresAt +} + +// GetAuthorizedCredentialID returns the AuthorizedCredentialID field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetAuthorizedCredentialID() int64 { + if c == nil || c.AuthorizedCredentialID == nil { + return 0 + } + return *c.AuthorizedCredentialID +} + +// GetAuthorizedCredentialNote returns the AuthorizedCredentialNote field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetAuthorizedCredentialNote() string { + if c == nil || c.AuthorizedCredentialNote == nil { + return "" + } + return *c.AuthorizedCredentialNote +} + +// GetAuthorizedCredentialTitle returns the AuthorizedCredentialTitle field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetAuthorizedCredentialTitle() string { + if c == nil || c.AuthorizedCredentialTitle == nil { + return "" + } + return *c.AuthorizedCredentialTitle +} + +// GetCredentialAccessedAt returns the CredentialAccessedAt field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetCredentialAccessedAt() Timestamp { + if c == nil || c.CredentialAccessedAt == nil { + return Timestamp{} + } + return *c.CredentialAccessedAt +} + +// GetCredentialAuthorizedAt returns the CredentialAuthorizedAt field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetCredentialAuthorizedAt() Timestamp { + if c == nil || c.CredentialAuthorizedAt == nil { + return Timestamp{} + } + return *c.CredentialAuthorizedAt +} + +// GetCredentialID returns the CredentialID field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetCredentialID() int64 { + if c == nil || c.CredentialID == nil { + return 0 + } + return *c.CredentialID +} + +// GetCredentialType returns the CredentialType field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetCredentialType() string { + if c == nil || c.CredentialType == nil { + return "" + } + return *c.CredentialType +} + +// GetFingerprint returns the Fingerprint field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetFingerprint() string { + if c == nil || c.Fingerprint == nil { + return "" + } + return *c.Fingerprint +} + +// GetLogin returns the Login field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetLogin() string { + if c == nil || c.Login == nil { + return "" + } + return *c.Login +} + +// GetTokenLastEight returns the TokenLastEight field if it's non-nil, zero value otherwise. +func (c *CredentialAuthorization) GetTokenLastEight() string { + if c == nil || c.TokenLastEight == nil { + return "" + } + return *c.TokenLastEight +} + // GetBaseRole returns the BaseRole field if it's non-nil, zero value otherwise. func (c *CustomRepoRoles) GetBaseRole() string { if c == nil || c.BaseRole == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 29d1fd4f1a5..75148227d3b 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -5508,6 +5508,116 @@ func TestCreateUserProjectOptions_GetBody(tt *testing.T) { c.GetBody() } +func TestCredentialAuthorization_GetAuthorizedCredentialExpiresAt(tt *testing.T) { + var zeroValue Timestamp + c := &CredentialAuthorization{AuthorizedCredentialExpiresAt: &zeroValue} + c.GetAuthorizedCredentialExpiresAt() + c = &CredentialAuthorization{} + c.GetAuthorizedCredentialExpiresAt() + c = nil + c.GetAuthorizedCredentialExpiresAt() +} + +func TestCredentialAuthorization_GetAuthorizedCredentialID(tt *testing.T) { + var zeroValue int64 + c := &CredentialAuthorization{AuthorizedCredentialID: &zeroValue} + c.GetAuthorizedCredentialID() + c = &CredentialAuthorization{} + c.GetAuthorizedCredentialID() + c = nil + c.GetAuthorizedCredentialID() +} + +func TestCredentialAuthorization_GetAuthorizedCredentialNote(tt *testing.T) { + var zeroValue string + c := &CredentialAuthorization{AuthorizedCredentialNote: &zeroValue} + c.GetAuthorizedCredentialNote() + c = &CredentialAuthorization{} + c.GetAuthorizedCredentialNote() + c = nil + c.GetAuthorizedCredentialNote() +} + +func TestCredentialAuthorization_GetAuthorizedCredentialTitle(tt *testing.T) { + var zeroValue string + c := &CredentialAuthorization{AuthorizedCredentialTitle: &zeroValue} + c.GetAuthorizedCredentialTitle() + c = &CredentialAuthorization{} + c.GetAuthorizedCredentialTitle() + c = nil + c.GetAuthorizedCredentialTitle() +} + +func TestCredentialAuthorization_GetCredentialAccessedAt(tt *testing.T) { + var zeroValue Timestamp + c := &CredentialAuthorization{CredentialAccessedAt: &zeroValue} + c.GetCredentialAccessedAt() + c = &CredentialAuthorization{} + c.GetCredentialAccessedAt() + c = nil + c.GetCredentialAccessedAt() +} + +func TestCredentialAuthorization_GetCredentialAuthorizedAt(tt *testing.T) { + var zeroValue Timestamp + c := &CredentialAuthorization{CredentialAuthorizedAt: &zeroValue} + c.GetCredentialAuthorizedAt() + c = &CredentialAuthorization{} + c.GetCredentialAuthorizedAt() + c = nil + c.GetCredentialAuthorizedAt() +} + +func TestCredentialAuthorization_GetCredentialID(tt *testing.T) { + var zeroValue int64 + c := &CredentialAuthorization{CredentialID: &zeroValue} + c.GetCredentialID() + c = &CredentialAuthorization{} + c.GetCredentialID() + c = nil + c.GetCredentialID() +} + +func TestCredentialAuthorization_GetCredentialType(tt *testing.T) { + var zeroValue string + c := &CredentialAuthorization{CredentialType: &zeroValue} + c.GetCredentialType() + c = &CredentialAuthorization{} + c.GetCredentialType() + c = nil + c.GetCredentialType() +} + +func TestCredentialAuthorization_GetFingerprint(tt *testing.T) { + var zeroValue string + c := &CredentialAuthorization{Fingerprint: &zeroValue} + c.GetFingerprint() + c = &CredentialAuthorization{} + c.GetFingerprint() + c = nil + c.GetFingerprint() +} + +func TestCredentialAuthorization_GetLogin(tt *testing.T) { + var zeroValue string + c := &CredentialAuthorization{Login: &zeroValue} + c.GetLogin() + c = &CredentialAuthorization{} + c.GetLogin() + c = nil + c.GetLogin() +} + +func TestCredentialAuthorization_GetTokenLastEight(tt *testing.T) { + var zeroValue string + c := &CredentialAuthorization{TokenLastEight: &zeroValue} + c.GetTokenLastEight() + c = &CredentialAuthorization{} + c.GetTokenLastEight() + c = nil + c.GetTokenLastEight() +} + func TestCustomRepoRoles_GetBaseRole(tt *testing.T) { var zeroValue string c := &CustomRepoRoles{BaseRole: &zeroValue} diff --git a/github/orgs_credential_authorizations.go b/github/orgs_credential_authorizations.go new file mode 100644 index 00000000000..88fccb8ad4d --- /dev/null +++ b/github/orgs_credential_authorizations.go @@ -0,0 +1,90 @@ +package github + +import ( + "context" + "fmt" + "net/http" +) + +// CredentialAuthorization represents a credential authorized through SAML SSO +type CredentialAuthorization struct { + // User login that owns the underlying credential. + Login *string `json:"login"` + + // Unique identifier for the credential. + CredentialID *int64 `json:"credential_id"` + + // Human-readable description of the credential type. + CredentialType *string `json:"credential_type"` + + // Last eight characters of the credential. + // Only included in responses with credential_type of personal access token. + TokenLastEight *string `json:"token_last_eight"` + + // Date when the credential was authorized for use. + CredentialAuthorizedAt *Timestamp `json:"credential_authorized_at"` + + // Date when the credential was last accessed. + // May be null if it was never accessed. + CredentialAccessedAt *Timestamp `json:"credential_accessed_at"` + + // List of oauth scopes the token has been granted. + Scopes []string `json:"scopes"` + + // Unique string to distinguish the credential. + // Only included in responses with credential_type of SSH Key. + Fingerprint *string `json:"fingerprint"` + + AuthorizedCredentialID *int64 `json:"authorized_credential_id"` + + // The title given to the ssh key. + // This will only be present when the credential is an ssh key. + AuthorizedCredentialTitle *string `json:"authorized_credential_title"` + + // The note given to the token. + // This will only be present when the credential is a token. + AuthorizedCredentialNote *string `json:"authorized_credential_note"` + + // The expiry for the token. + // This will only be present when the credential is a token. + AuthorizedCredentialExpiresAt *Timestamp `json:"authorized_credential_expires_at"` +} + +// ListCredentialAuthorizations lists credentials authorized through SAML SSO +// for a given organization. Only available with GitHub Enterprise Cloud. +// +// GitHub API docs: https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/orgs?apiVersion=2022-11-28#list-saml-sso-authorizations-for-an-organization +func (s *OrganizationsService) ListCredentialAuthorizations(ctx context.Context, org string, opts *ListOptions) ([]*CredentialAuthorization, *Response, error) { + u := fmt.Sprintf("orgs/%v/credential-authorizations", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, nil, err + } + + var creds []*CredentialAuthorization + resp, err := s.client.Do(ctx, req, &creds) + if err != nil { + return nil, resp, err + } + + return creds, resp, nil +} + +// RemoveCredentialAuthorization revokes the SAML SSO authorization for a given +// credential within an organization. Only available with GitHub Enterprise Cloud. +// +// GitHub API docs: https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/orgs?apiVersion=2022-11-28#remove-a-saml-sso-authorization-for-an-organization +func (s *OrganizationsService) RemoveCredentialAuthorization(ctx context.Context, org string, credentialID int64) (*Response, error) { + u := fmt.Sprintf("orgs/%v/credential-authorizations/%v", org, credentialID) + req, err := s.client.NewRequest(http.MethodDelete, u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/github/orgs_credential_authorizations_test.go b/github/orgs_credential_authorizations_test.go new file mode 100644 index 00000000000..5ed312d427c --- /dev/null +++ b/github/orgs_credential_authorizations_test.go @@ -0,0 +1,92 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestOrganizationsService_ListCredentialAuthorizations(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/credential-authorizations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodGet) + fmt.Fprint(w, `[ + { + "login": "l", + "credential_id": 1, + "credential_type": "t", + "credential_authorized_at": "2017-01-21T00:00:00Z", + "credential_accessed_at": "2017-01-21T00:00:00Z", + "authorized_credential_id": 1 + } + ]`) + }) + + ctx := context.Background() + creds, _, err := client.Organizations.ListCredentialAuthorizations(ctx, "o", nil) + if err != nil { + t.Errorf("Organizations.ListCredentialAuthorizations returned error: %v", err) + } + + ts := time.Date(2017, time.January, 21, 0, 0, 0, 0, time.UTC) + want := []*CredentialAuthorization{ + { + Login: String("l"), + CredentialID: Int64(1), + CredentialType: String("t"), + CredentialAuthorizedAt: &Timestamp{ts}, + CredentialAccessedAt: &Timestamp{ts}, + AuthorizedCredentialID: Int64(1), + }, + } + if !cmp.Equal(creds, want) { + t.Errorf("Organizations.ListCredentialAuthorizations returned %+v, want %+v", creds, want) + } + + const methodName = "ListCredentialAuthorizations" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Organizations.ListCredentialAuthorizations(ctx, "\n", nil) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.Organizations.ListCredentialAuthorizations(ctx, "o", nil) + return resp, err + }) +} + +func TestOrganizationsService_RemoveCredentialAuthorization(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/credential-authorizations/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, http.MethodDelete) + w.WriteHeader(http.StatusNoContent) + }) + + ctx := context.Background() + resp, err := client.Organizations.RemoveCredentialAuthorization(ctx, "o", 1) + if err != nil { + t.Errorf("Organizations.RemoveCredentialAuthorization returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Organizations.RemoveCredentialAuthorization returned %v, want %v", resp.StatusCode, http.StatusNoContent) + } + + const methodName = "RemoveCredentialAuthorization" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Organizations.RemoveCredentialAuthorization(ctx, "\n", 0) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Organizations.RemoveCredentialAuthorization(ctx, "o", 1) + }) +} From cfba85a958a2aee3ed8fe6bcdc8cbccda51c72fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Penteado?= <4219131+joaopenteado@users.noreply.github.com> Date: Fri, 21 Jul 2023 10:24:42 +0900 Subject: [PATCH 2/4] Applied suggestions from code review Co-authored-by: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> --- github/orgs_credential_authorizations.go | 7 ++++++- github/orgs_credential_authorizations_test.go | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/github/orgs_credential_authorizations.go b/github/orgs_credential_authorizations.go index 88fccb8ad4d..a1c08128af1 100644 --- a/github/orgs_credential_authorizations.go +++ b/github/orgs_credential_authorizations.go @@ -1,3 +1,8 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package github import ( @@ -6,7 +11,7 @@ import ( "net/http" ) -// CredentialAuthorization represents a credential authorized through SAML SSO +// CredentialAuthorization represents a credential authorized through SAML SSO. type CredentialAuthorization struct { // User login that owns the underlying credential. Login *string `json:"login"` diff --git a/github/orgs_credential_authorizations_test.go b/github/orgs_credential_authorizations_test.go index 5ed312d427c..df44db7cba3 100644 --- a/github/orgs_credential_authorizations_test.go +++ b/github/orgs_credential_authorizations_test.go @@ -1,3 +1,8 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package github import ( From 756d7b4aa32580b09f19d7dfe2eef1d3e7fe597e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Penteado?= <4219131+joaopenteado@users.noreply.github.com> Date: Fri, 21 Jul 2023 10:28:22 +0900 Subject: [PATCH 3/4] Added `omitempty` to `CredentialAuthorization` struct members --- github/orgs_credential_authorizations.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/github/orgs_credential_authorizations.go b/github/orgs_credential_authorizations.go index a1c08128af1..6d56fe8f79a 100644 --- a/github/orgs_credential_authorizations.go +++ b/github/orgs_credential_authorizations.go @@ -14,45 +14,45 @@ import ( // CredentialAuthorization represents a credential authorized through SAML SSO. type CredentialAuthorization struct { // User login that owns the underlying credential. - Login *string `json:"login"` + Login *string `json:"login,omitempty"` // Unique identifier for the credential. - CredentialID *int64 `json:"credential_id"` + CredentialID *int64 `json:"credential_id,omitempty"` // Human-readable description of the credential type. - CredentialType *string `json:"credential_type"` + CredentialType *string `json:"credential_type,omitempty"` // Last eight characters of the credential. // Only included in responses with credential_type of personal access token. - TokenLastEight *string `json:"token_last_eight"` + TokenLastEight *string `json:"token_last_eight,omitempty"` // Date when the credential was authorized for use. - CredentialAuthorizedAt *Timestamp `json:"credential_authorized_at"` + CredentialAuthorizedAt *Timestamp `json:"credential_authorized_at,omitempty"` // Date when the credential was last accessed. // May be null if it was never accessed. - CredentialAccessedAt *Timestamp `json:"credential_accessed_at"` + CredentialAccessedAt *Timestamp `json:"credential_accessed_at,omitempty"` // List of oauth scopes the token has been granted. - Scopes []string `json:"scopes"` + Scopes []string `json:"scopes,omitempty"` // Unique string to distinguish the credential. // Only included in responses with credential_type of SSH Key. - Fingerprint *string `json:"fingerprint"` + Fingerprint *string `json:"fingerprint,omitempty"` - AuthorizedCredentialID *int64 `json:"authorized_credential_id"` + AuthorizedCredentialID *int64 `json:"authorized_credential_id,omitempty"` // The title given to the ssh key. // This will only be present when the credential is an ssh key. - AuthorizedCredentialTitle *string `json:"authorized_credential_title"` + AuthorizedCredentialTitle *string `json:"authorized_credential_title,omitempty"` // The note given to the token. // This will only be present when the credential is a token. - AuthorizedCredentialNote *string `json:"authorized_credential_note"` + AuthorizedCredentialNote *string `json:"authorized_credential_note,omitempty"` // The expiry for the token. // This will only be present when the credential is a token. - AuthorizedCredentialExpiresAt *Timestamp `json:"authorized_credential_expires_at"` + AuthorizedCredentialExpiresAt *Timestamp `json:"authorized_credential_expires_at,omitempty"` } // ListCredentialAuthorizations lists credentials authorized through SAML SSO From 529eb799de7ac2a57e1bad47751004cc62117783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Penteado?= <4219131+joaopenteado@users.noreply.github.com> Date: Fri, 21 Jul 2023 12:24:20 +0900 Subject: [PATCH 4/4] Added tests for `ListOptions` in `ListCredentialAuthorizations` --- github/orgs_credential_authorizations_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/github/orgs_credential_authorizations_test.go b/github/orgs_credential_authorizations_test.go index df44db7cba3..e3986eb5ca2 100644 --- a/github/orgs_credential_authorizations_test.go +++ b/github/orgs_credential_authorizations_test.go @@ -21,6 +21,7 @@ func TestOrganizationsService_ListCredentialAuthorizations(t *testing.T) { mux.HandleFunc("/orgs/o/credential-authorizations", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) + testFormValues(t, r, values{"per_page": "2", "page": "2"}) fmt.Fprint(w, `[ { "login": "l", @@ -33,8 +34,9 @@ func TestOrganizationsService_ListCredentialAuthorizations(t *testing.T) { ]`) }) + opts := &ListOptions{Page: 2, PerPage: 2} ctx := context.Background() - creds, _, err := client.Organizations.ListCredentialAuthorizations(ctx, "o", nil) + creds, _, err := client.Organizations.ListCredentialAuthorizations(ctx, "o", opts) if err != nil { t.Errorf("Organizations.ListCredentialAuthorizations returned error: %v", err) } @@ -56,12 +58,12 @@ func TestOrganizationsService_ListCredentialAuthorizations(t *testing.T) { const methodName = "ListCredentialAuthorizations" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Organizations.ListCredentialAuthorizations(ctx, "\n", nil) + _, _, err = client.Organizations.ListCredentialAuthorizations(ctx, "\n", opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - _, resp, err := client.Organizations.ListCredentialAuthorizations(ctx, "o", nil) + _, resp, err := client.Organizations.ListCredentialAuthorizations(ctx, "o", opts) return resp, err }) }