mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-08 23:23:39 +00:00
upstreamgitub.go now uses githubclient to determine username and groups
This commit is contained in:
@@ -24,3 +24,10 @@ func OIDC(upstreamIssuerAsString string, upstreamSubject string, idpDisplayName
|
||||
oidc.IDTokenClaimSubject, url.QueryEscape(upstreamSubject),
|
||||
)
|
||||
}
|
||||
|
||||
func GitHub(APIBaseURL, idpDisplayName, login, id string) string {
|
||||
return fmt.Sprintf("%s?%s=%s&login=%s&id=%s", APIBaseURL,
|
||||
oidc.IDTokenSubClaimIDPNameQueryParam, url.QueryEscape(idpDisplayName),
|
||||
url.QueryEscape(login), url.QueryEscape(id),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,3 +89,41 @@ func TestOIDC(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHub(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
APIBaseURL string
|
||||
idpDisplayName string
|
||||
login string
|
||||
id string
|
||||
wantSubject string
|
||||
}{
|
||||
{
|
||||
name: "simple display name",
|
||||
APIBaseURL: "https://github.com",
|
||||
idpDisplayName: "simpleName",
|
||||
login: "some login",
|
||||
id: "some id",
|
||||
wantSubject: "https://github.com?idpName=simpleName&login=some+login&id=some+id",
|
||||
},
|
||||
{
|
||||
name: "interesting display name",
|
||||
APIBaseURL: "https://server.example.com:1234/path",
|
||||
idpDisplayName: "this is a 👍 display name that 🦭 can handle",
|
||||
login: "some other login",
|
||||
id: "some other id",
|
||||
wantSubject: "https://server.example.com:1234/path?idpName=this+is+a+%F0%9F%91%8D+display+name+that+%F0%9F%A6%AD+can+handle&login=some+other+login&id=some+other+id",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
actual := GitHub(test.APIBaseURL, test.idpDisplayName, test.login, test.id)
|
||||
|
||||
require.Equal(t, test.wantSubject, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
6
internal/mocks/mockgithubclient/generate.go
Normal file
6
internal/mocks/mockgithubclient/generate.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package mockgithubclient
|
||||
|
||||
//go:generate go run -v go.uber.org/mock/mockgen -destination=mockgithubclient.go -package=mockgithubclient -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/githubclient GitHubInterface
|
||||
91
internal/mocks/mockgithubclient/mockgithubclient.go
Normal file
91
internal/mocks/mockgithubclient/mockgithubclient.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: go.pinniped.dev/internal/githubclient (interfaces: GitHubInterface)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination=mockgithubclient.go -package=mockgithubclient -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/githubclient GitHubInterface
|
||||
//
|
||||
|
||||
// Package mockgithubclient is a generated GoMock package.
|
||||
package mockgithubclient
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
githubclient "go.pinniped.dev/internal/githubclient"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
sets "k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// MockGitHubInterface is a mock of GitHubInterface interface.
|
||||
type MockGitHubInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockGitHubInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockGitHubInterfaceMockRecorder is the mock recorder for MockGitHubInterface.
|
||||
type MockGitHubInterfaceMockRecorder struct {
|
||||
mock *MockGitHubInterface
|
||||
}
|
||||
|
||||
// NewMockGitHubInterface creates a new mock instance.
|
||||
func NewMockGitHubInterface(ctrl *gomock.Controller) *MockGitHubInterface {
|
||||
mock := &MockGitHubInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockGitHubInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockGitHubInterface) EXPECT() *MockGitHubInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetOrgMembership mocks base method.
|
||||
func (m *MockGitHubInterface) GetOrgMembership(arg0 context.Context) (sets.Set[string], error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOrgMembership", arg0)
|
||||
ret0, _ := ret[0].(sets.Set[string])
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOrgMembership indicates an expected call of GetOrgMembership.
|
||||
func (mr *MockGitHubInterfaceMockRecorder) GetOrgMembership(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrgMembership", reflect.TypeOf((*MockGitHubInterface)(nil).GetOrgMembership), arg0)
|
||||
}
|
||||
|
||||
// GetTeamMembership mocks base method.
|
||||
func (m *MockGitHubInterface) GetTeamMembership(arg0 context.Context, arg1 sets.Set[string]) ([]*githubclient.TeamInfo, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTeamMembership", arg0, arg1)
|
||||
ret0, _ := ret[0].([]*githubclient.TeamInfo)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTeamMembership indicates an expected call of GetTeamMembership.
|
||||
func (mr *MockGitHubInterfaceMockRecorder) GetTeamMembership(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeamMembership", reflect.TypeOf((*MockGitHubInterface)(nil).GetTeamMembership), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUserInfo mocks base method.
|
||||
func (m *MockGitHubInterface) GetUserInfo(arg0 context.Context) (*githubclient.UserInfo, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserInfo", arg0)
|
||||
ret0, _ := ret[0].(*githubclient.UserInfo)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserInfo indicates an expected call of GetUserInfo.
|
||||
func (mr *MockGitHubInterfaceMockRecorder) GetUserInfo(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInfo", reflect.TypeOf((*MockGitHubInterface)(nil).GetUserInfo), arg0)
|
||||
}
|
||||
@@ -6,14 +6,19 @@ package upstreamgithub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
supervisoridpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/federationdomain/downstreamsubject"
|
||||
"go.pinniped.dev/internal/federationdomain/upstreamprovider"
|
||||
"go.pinniped.dev/internal/githubclient"
|
||||
)
|
||||
|
||||
// ProviderConfig holds the active configuration of an upstream GitHub provider.
|
||||
@@ -26,8 +31,8 @@ type ProviderConfig struct {
|
||||
// or https://HOSTNAME/api/v3/ for Enterprise Server.
|
||||
APIBaseURL string
|
||||
|
||||
UsernameAttribute v1alpha1.GitHubUsernameAttribute
|
||||
GroupNameAttribute v1alpha1.GitHubGroupNameAttribute
|
||||
UsernameAttribute supervisoridpv1alpha1.GitHubUsernameAttribute
|
||||
GroupNameAttribute supervisoridpv1alpha1.GitHubGroupNameAttribute
|
||||
|
||||
// AllowedOrganizations, when empty, means to allow users from all orgs.
|
||||
AllowedOrganizations []string
|
||||
@@ -46,7 +51,8 @@ type ProviderConfig struct {
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
c ProviderConfig
|
||||
c ProviderConfig
|
||||
buildGitHubClient func(httpClient *http.Client, apiBaseURL, token string) (githubclient.GitHubInterface, error)
|
||||
}
|
||||
|
||||
var _ upstreamprovider.UpstreamGithubIdentityProviderI = &Provider{}
|
||||
@@ -54,7 +60,10 @@ var _ upstreamprovider.UpstreamGithubIdentityProviderI = &Provider{}
|
||||
// New creates a Provider. The config is not a pointer to ensure that a copy of the config is created,
|
||||
// making the resulting Provider use an effectively read-only configuration.
|
||||
func New(config ProviderConfig) *Provider {
|
||||
return &Provider{c: config}
|
||||
return &Provider{
|
||||
c: config,
|
||||
buildGitHubClient: githubclient.NewGitHubClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) GetName() string {
|
||||
@@ -73,11 +82,11 @@ func (p *Provider) GetScopes() []string {
|
||||
return p.c.OAuth2Config.Scopes
|
||||
}
|
||||
|
||||
func (p *Provider) GetUsernameAttribute() v1alpha1.GitHubUsernameAttribute {
|
||||
func (p *Provider) GetUsernameAttribute() supervisoridpv1alpha1.GitHubUsernameAttribute {
|
||||
return p.c.UsernameAttribute
|
||||
}
|
||||
|
||||
func (p *Provider) GetGroupNameAttribute() v1alpha1.GitHubGroupNameAttribute {
|
||||
func (p *Provider) GetGroupNameAttribute() supervisoridpv1alpha1.GitHubGroupNameAttribute {
|
||||
return p.c.GroupNameAttribute
|
||||
}
|
||||
|
||||
@@ -104,19 +113,73 @@ func (p *Provider) ExchangeAuthcode(ctx context.Context, authcode string, redire
|
||||
return tok.AccessToken, nil
|
||||
}
|
||||
|
||||
func (p *Provider) GetUser(_ctx context.Context, _accessToken string) (*upstreamprovider.GitHubUser, error) {
|
||||
// TODO Implement this to make several https calls to github to learn about the user, using a lower-level githubclient package.
|
||||
// Pass the ctx, accessToken, p.c.HttpClient, and p.c.APIBaseURL to the lower-level package's functions.
|
||||
// TODO: Reject the auth if the user does not belong to any of p.c.AllowedOrganizations (unless p.c.AllowedOrganizations is empty).
|
||||
// TODO: Make use of p.c.UsernameAttribute and p.c.GroupNameAttribute when deciding the username and group names.
|
||||
// TODO: Determine the downstream subject by first writing a helper in downstream_subject.go and then calling it here.
|
||||
panic("implement me")
|
||||
//nolint:govet // this code is intentionally unreachable until we resolve the todos
|
||||
return &upstreamprovider.GitHubUser{
|
||||
Username: "TODO",
|
||||
Groups: []string{"org/TODO"},
|
||||
DownstreamSubject: "TODO",
|
||||
}, nil
|
||||
// GetUser will use the provided configuration to make HTTPS calls to the GitHub API to find out who the logged-in user is,
|
||||
// what organizations they belong to, and what teams they belong to.
|
||||
// If the user's information meets the AllowedOrganization criteria specified on the GitHubIdentityProvider, they will be
|
||||
// allowed to log in.
|
||||
// Note that errors from the githubclient package already have helpful error prefixes, so there is no need for additional prefixes here.
|
||||
// TODO: populate the IDP display name
|
||||
// TODO: What should we do if the group or team name is outside of the enum? The controller would reject this.
|
||||
// TODO: should we use the APIBaseURL or some other URL in the downstreamSubject
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// "github.com" or "https://github.com" or "https://api.github.com"?
|
||||
// "enterprise.tld" or "https://enterprise.tld" or "https://enterprise.tld/api/v3"?
|
||||
func (p *Provider) GetUser(ctx context.Context, accessToken string) (*upstreamprovider.GitHubUser, error) {
|
||||
githubClient, err := p.buildGitHubClient(p.c.HttpClient, p.c.APIBaseURL, accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
githubUser := upstreamprovider.GitHubUser{}
|
||||
|
||||
userInfo, err := githubClient.GetUserInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
githubUser.DownstreamSubject = downstreamsubject.GitHub(p.c.APIBaseURL, "TODO_IDP_DISPLAY_NAME", userInfo.Login, userInfo.ID)
|
||||
|
||||
switch p.c.UsernameAttribute {
|
||||
case supervisoridpv1alpha1.GitHubUsernameLoginAndID:
|
||||
githubUser.Username = fmt.Sprintf("%s:%s", userInfo.Login, userInfo.ID)
|
||||
case supervisoridpv1alpha1.GitHubUsernameLogin:
|
||||
githubUser.Username = userInfo.Login
|
||||
case supervisoridpv1alpha1.GitHubUsernameID:
|
||||
githubUser.Username = userInfo.ID
|
||||
}
|
||||
|
||||
orgMembership, err := githubClient.GetOrgMembership(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowedOrgs := sets.New[string](p.c.AllowedOrganizations...)
|
||||
|
||||
if allowedOrgs.Len() > 0 && allowedOrgs.Intersection(orgMembership).Len() < 1 {
|
||||
return nil, errors.New("user is not allowed to log in due to organization membership policy")
|
||||
}
|
||||
|
||||
teamMembership, err := githubClient.GetTeamMembership(ctx, allowedOrgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, team := range teamMembership {
|
||||
downstreamGroup := ""
|
||||
|
||||
switch p.c.GroupNameAttribute {
|
||||
case supervisoridpv1alpha1.GitHubUseTeamNameForGroupName:
|
||||
downstreamGroup = fmt.Sprintf("%s/%s", team.Org, team.Name)
|
||||
case supervisoridpv1alpha1.GitHubUseTeamSlugForGroupName:
|
||||
downstreamGroup = fmt.Sprintf("%s/%s", team.Org, team.Slug)
|
||||
}
|
||||
|
||||
githubUser.Groups = append(githubUser.Groups, downstreamGroup)
|
||||
}
|
||||
|
||||
return &githubUser, nil
|
||||
}
|
||||
|
||||
// GetConfig returns the config. This is not part of the UpstreamGithubIdentityProviderI interface and is just for testing.
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
package upstreamgithub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/mock/gomock"
|
||||
"golang.org/x/oauth2"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
supervisoridpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/federationdomain/upstreamprovider"
|
||||
"go.pinniped.dev/internal/githubclient"
|
||||
"go.pinniped.dev/internal/mocks/mockgithubclient"
|
||||
)
|
||||
|
||||
func TestGitHubProvider(t *testing.T) {
|
||||
@@ -65,11 +73,282 @@ func TestGitHubProvider(t *testing.T) {
|
||||
require.Equal(t, types.UID("resource-uid-12345"), subject.GetResourceUID())
|
||||
require.Equal(t, "fake-client-id", subject.GetClientID())
|
||||
require.Equal(t, "fake-client-id", subject.GetClientID())
|
||||
require.Equal(t, v1alpha1.GitHubUsernameAttribute("fake-username-attribute"), subject.GetUsernameAttribute())
|
||||
require.Equal(t, v1alpha1.GitHubGroupNameAttribute("fake-group-name-attribute"), subject.GetGroupNameAttribute())
|
||||
require.Equal(t, supervisoridpv1alpha1.GitHubUsernameAttribute("fake-username-attribute"), subject.GetUsernameAttribute())
|
||||
require.Equal(t, supervisoridpv1alpha1.GitHubGroupNameAttribute("fake-group-name-attribute"), subject.GetGroupNameAttribute())
|
||||
require.Equal(t, []string{"fake-org", "fake-org2"}, subject.GetAllowedOrganizations())
|
||||
require.Equal(t, "https://fake-authorization-url", subject.GetAuthorizationURL())
|
||||
require.Equal(t, &http.Client{
|
||||
Timeout: 1234509,
|
||||
}, subject.GetConfig().HttpClient)
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
t.Cleanup(ctrl.Finish)
|
||||
|
||||
someContext := context.Background()
|
||||
|
||||
someHttpClient := &http.Client{
|
||||
Timeout: 1234509,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerConfig ProviderConfig
|
||||
buildGitHubClientError error
|
||||
buildMockResponses func(hubInterface *mockgithubclient.MockGitHubInterface)
|
||||
wantUser *upstreamprovider.GitHubUser
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "happy path with username=login:id",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, sets.New[string]()).Return(nil, nil)
|
||||
},
|
||||
wantUser: &upstreamprovider.GitHubUser{
|
||||
Username: "some-github-login:some-github-id",
|
||||
DownstreamSubject: "https://some-url?idpName=TODO_IDP_DISPLAY_NAME&login=some-github-login&id=some-github-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with username=login",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLogin,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, sets.New[string]()).Return(nil, nil)
|
||||
},
|
||||
wantUser: &upstreamprovider.GitHubUser{
|
||||
Username: "some-github-login",
|
||||
DownstreamSubject: "https://some-url?idpName=TODO_IDP_DISPLAY_NAME&login=some-github-login&id=some-github-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with username=id",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameID,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, sets.New[string]()).Return(nil, nil)
|
||||
},
|
||||
wantUser: &upstreamprovider.GitHubUser{
|
||||
Username: "some-github-id",
|
||||
DownstreamSubject: "https://some-url?idpName=TODO_IDP_DISPLAY_NAME&login=some-github-login&id=some-github-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with user in allowed organizations",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID,
|
||||
AllowedOrganizations: []string{"allowed-org1", "allowed-org2"},
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(sets.New[string]("allowed-org2"), nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, sets.New[string]("allowed-org1", "allowed-org2")).Return(nil, nil)
|
||||
},
|
||||
wantUser: &upstreamprovider.GitHubUser{
|
||||
Username: "some-github-login:some-github-id",
|
||||
DownstreamSubject: "https://some-url?idpName=TODO_IDP_DISPLAY_NAME&login=some-github-login&id=some-github-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns error when the user does not belong to the allowed organizations",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameID,
|
||||
AllowedOrganizations: []string{"allowed-org"},
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(sets.New[string]("disallowed-org"), nil)
|
||||
},
|
||||
wantErr: "user is not allowed to log in due to organization membership policy",
|
||||
},
|
||||
{
|
||||
name: "happy path with groups=name",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID,
|
||||
AllowedOrganizations: []string{"allowed-org1", "allowed-org2"},
|
||||
GroupNameAttribute: supervisoridpv1alpha1.GitHubUseTeamNameForGroupName,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(sets.New[string]("allowed-org2"), nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, sets.New[string]("allowed-org1", "allowed-org2")).Return([]*githubclient.TeamInfo{
|
||||
{
|
||||
Name: "org1-team1-name",
|
||||
Slug: "org1-team1-slug",
|
||||
Org: "org1-name",
|
||||
},
|
||||
{
|
||||
Name: "org1-team2-name",
|
||||
Slug: "org1-team2-slug",
|
||||
Org: "org1-name",
|
||||
},
|
||||
{
|
||||
Name: "org2-team1-name",
|
||||
Slug: "org2-team1-slug",
|
||||
Org: "org2-name",
|
||||
},
|
||||
}, nil)
|
||||
},
|
||||
wantUser: &upstreamprovider.GitHubUser{
|
||||
Username: "some-github-login:some-github-id",
|
||||
Groups: []string{"org1-name/org1-team1-name", "org1-name/org1-team2-name", "org2-name/org2-team1-name"},
|
||||
DownstreamSubject: "https://some-url?idpName=TODO_IDP_DISPLAY_NAME&login=some-github-login&id=some-github-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with groups=slug",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
UsernameAttribute: supervisoridpv1alpha1.GitHubUsernameLoginAndID,
|
||||
AllowedOrganizations: []string{"allowed-org1", "allowed-org2"},
|
||||
GroupNameAttribute: supervisoridpv1alpha1.GitHubUseTeamSlugForGroupName,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{
|
||||
Login: "some-github-login",
|
||||
ID: "some-github-id",
|
||||
}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(sets.New[string]("allowed-org2"), nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, sets.New[string]("allowed-org1", "allowed-org2")).Return([]*githubclient.TeamInfo{
|
||||
{
|
||||
Name: "org1-team1-name",
|
||||
Slug: "org1-team1-slug",
|
||||
Org: "org1-name",
|
||||
},
|
||||
{
|
||||
Name: "org1-team2-name",
|
||||
Slug: "org1-team2-slug",
|
||||
Org: "org1-name",
|
||||
},
|
||||
{
|
||||
Name: "org2-team1-name",
|
||||
Slug: "org2-team1-slug",
|
||||
Org: "org2-name",
|
||||
},
|
||||
}, nil)
|
||||
},
|
||||
wantUser: &upstreamprovider.GitHubUser{
|
||||
Username: "some-github-login:some-github-id",
|
||||
Groups: []string{"org1-name/org1-team1-slug", "org1-name/org1-team2-slug", "org2-name/org2-team1-slug"},
|
||||
DownstreamSubject: "https://some-url?idpName=TODO_IDP_DISPLAY_NAME&login=some-github-login&id=some-github-id",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "returns errors from buildGitHubClient()",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
},
|
||||
buildGitHubClientError: errors.New("error from building a github client"),
|
||||
wantErr: "error from building a github client",
|
||||
},
|
||||
{
|
||||
name: "returns errors from githubClient.GetUserInfo()",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(nil, errors.New("error from githubClient.GetUserInfo"))
|
||||
},
|
||||
wantErr: "error from githubClient.GetUserInfo",
|
||||
},
|
||||
{
|
||||
name: "returns errors from githubClient.GetOrgMembership()",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, errors.New("error from githubClient.GetOrgMembership"))
|
||||
},
|
||||
wantErr: "error from githubClient.GetOrgMembership",
|
||||
},
|
||||
{
|
||||
name: "returns errors from githubClient.GetTeamMembership()",
|
||||
providerConfig: ProviderConfig{
|
||||
APIBaseURL: "https://some-url",
|
||||
HttpClient: someHttpClient,
|
||||
},
|
||||
buildMockResponses: func(mockGitHubInterface *mockgithubclient.MockGitHubInterface) {
|
||||
mockGitHubInterface.EXPECT().GetUserInfo(someContext).Return(&githubclient.UserInfo{}, nil)
|
||||
mockGitHubInterface.EXPECT().GetOrgMembership(someContext).Return(nil, nil)
|
||||
mockGitHubInterface.EXPECT().GetTeamMembership(someContext, gomock.Any()).Return(nil, errors.New("error from githubClient.GetTeamMembership"))
|
||||
},
|
||||
wantErr: "error from githubClient.GetTeamMembership",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
accessToken := "some-opaque-github-access-token" + rand.String(8)
|
||||
mockGitHubInterface := mockgithubclient.NewMockGitHubInterface(ctrl)
|
||||
if test.buildMockResponses != nil {
|
||||
test.buildMockResponses(mockGitHubInterface)
|
||||
}
|
||||
|
||||
p := New(test.providerConfig)
|
||||
p.buildGitHubClient = func(httpClient *http.Client, apiBaseURL, token string) (githubclient.GitHubInterface, error) {
|
||||
require.Equal(t, test.providerConfig.HttpClient, httpClient)
|
||||
require.Equal(t, test.providerConfig.APIBaseURL, apiBaseURL)
|
||||
require.Equal(t, accessToken, token)
|
||||
|
||||
return mockGitHubInterface, test.buildGitHubClientError
|
||||
}
|
||||
|
||||
actualUser, actualErr := p.GetUser(context.Background(), accessToken)
|
||||
if test.wantErr != "" {
|
||||
require.EqualError(t, actualErr, test.wantErr)
|
||||
require.Nil(t, actualUser)
|
||||
return
|
||||
}
|
||||
require.NoError(t, actualErr)
|
||||
require.Equal(t, test.wantUser, actualUser)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user