refactor upstreamgithub.ProviderConfig to hold more config

This commit is contained in:
Ryan Richard
2024-05-08 11:38:38 -07:00
parent 29eb3dd384
commit 7277d00e1a
11 changed files with 341 additions and 193 deletions

View File

@@ -58,6 +58,9 @@ const (
ClientCredentialsObtained string = "ClientCredentialsObtained" //nolint:gosec // this is not a credential
GitHubConnectionValid string = "GitHubConnectionValid"
ClaimsValid string = "ClaimsValid"
defaultHost = "github.com"
defaultApiBaseURL = "https://api.github.com"
)
// UpstreamGitHubIdentityProviderICache is a thread safe cache that holds a list of validated upstream GitHub IDP configurations.
@@ -73,6 +76,7 @@ type gitHubWatcherController struct {
gitHubIdentityProviderInformer idpinformers.GitHubIdentityProviderInformer
secretInformer corev1informers.SecretInformer
clock clock.Clock
dialFunc func(network, addr string, config *tls.Config) (*tls.Conn, error)
}
// New instantiates a new controllerlib.Controller which will populate the provided UpstreamGitHubIdentityProviderICache.
@@ -85,6 +89,7 @@ func New(
log plog.Logger,
withInformer pinnipedcontroller.WithInformerOptionFunc,
clock clock.Clock,
dialFunc func(network, addr string, config *tls.Config) (*tls.Conn, error),
) controllerlib.Controller {
c := gitHubWatcherController{
namespace: namespace,
@@ -94,6 +99,7 @@ func New(
gitHubIdentityProviderInformer: gitHubIdentityProviderInformer,
secretInformer: secretInformer,
clock: clock,
dialFunc: dialFunc,
}
return controllerlib.New(
@@ -202,7 +208,7 @@ func (c *gitHubWatcherController) validateClientSecret(secretName string) (*meta
}, clientID, clientSecret, nil
}
func validateOrganizationsPolicy(organizationsSpec *v1alpha1.GitHubOrganizationsSpec) (*metav1.Condition, v1alpha1.GitHubAllowedAuthOrganizationsPolicy) {
func validateOrganizationsPolicy(organizationsSpec *v1alpha1.GitHubOrganizationsSpec) *metav1.Condition {
var policy v1alpha1.GitHubAllowedAuthOrganizationsPolicy
if organizationsSpec.Policy != nil {
policy = *organizationsSpec.Policy
@@ -217,7 +223,7 @@ func validateOrganizationsPolicy(organizationsSpec *v1alpha1.GitHubOrganizations
Status: metav1.ConditionTrue,
Reason: upstreamwatchers.ReasonSuccess,
Message: fmt.Sprintf("spec.allowAuthentication.organizations.policy (%q) is valid", policy),
}, policy
}
}
if len(organizationsSpec.Allowed) > 0 {
@@ -226,7 +232,7 @@ func validateOrganizationsPolicy(organizationsSpec *v1alpha1.GitHubOrganizations
Status: metav1.ConditionFalse,
Reason: "Invalid",
Message: "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",
}, policy
}
}
return &metav1.Condition{
@@ -234,7 +240,7 @@ func validateOrganizationsPolicy(organizationsSpec *v1alpha1.GitHubOrganizations
Status: metav1.ConditionFalse,
Reason: "Invalid",
Message: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",
}, policy
}
}
func (c *gitHubWatcherController) validateUpstreamAndUpdateConditions(ctx controllerlib.Context, upstream *v1alpha1.GitHubIdentityProvider) (
@@ -256,7 +262,7 @@ func (c *gitHubWatcherController) validateUpstreamAndUpdateConditions(ctx contro
userAndGroupCondition, groupNameAttribute, usernameAttribute := validateUserAndGroupAttributes(upstream)
conditions = append(conditions, userAndGroupCondition)
organizationPolicyCondition, policy := validateOrganizationsPolicy(&upstream.Spec.AllowAuthentication.Organizations)
organizationPolicyCondition := validateOrganizationsPolicy(&upstream.Spec.AllowAuthentication.Organizations)
conditions = append(conditions, organizationPolicyCondition)
hostCondition, hostPort := validateHost(upstream.Spec.GitHubAPI)
@@ -295,22 +301,36 @@ func (c *gitHubWatcherController) validateUpstreamAndUpdateConditions(ctx contro
upstreamgithub.ProviderConfig{
Name: upstream.Name,
ResourceUID: upstream.UID,
Host: hostURL,
APIBaseURL: apiBaseUrl(*upstream.Spec.GitHubAPI.Host, hostURL),
GroupNameAttribute: groupNameAttribute,
UsernameAttribute: usernameAttribute,
OAuth2Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
// See https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", hostURL),
DeviceAuthURL: "", // we do not use device code flow
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", hostURL),
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // this will be different for each FederationDomain, so we do not set it here
Scopes: []string{"read:user", "read:org"},
},
AllowedOrganizations: upstream.Spec.AllowAuthentication.Organizations.Allowed,
OrganizationLoginPolicy: policy,
AuthorizationURL: fmt.Sprintf("%s/login/oauth/authorize", hostURL),
HttpClient: httpClient,
AllowedOrganizations: upstream.Spec.AllowAuthentication.Organizations.Allowed,
HttpClient: httpClient,
},
)
return provider, k8sutilerrors.NewAggregate(applicationErrors)
}
func apiBaseUrl(upstreamSpecHost string, hostURL string) string {
if upstreamSpecHost != defaultHost {
return fmt.Sprintf("%s/api/v3", hostURL)
}
return defaultApiBaseURL
}
func validateHost(gitHubAPIConfig v1alpha1.GitHubAPIConfig) (*metav1.Condition, *endpointaddr.HostPort) {
buildInvalidHost := func(host, reason string) *metav1.Condition {
return &metav1.Condition{
@@ -376,7 +396,7 @@ func (c *gitHubWatcherController) validateGitHubConnection(
}, "", nil, nil
}
conn, tlsDialErr := tls.Dial("tcp", hostPort.Endpoint(), ptls.Default(certPool))
conn, tlsDialErr := c.dialFunc("tcp", hostPort.Endpoint(), ptls.Default(certPool))
if tlsDialErr != nil {
return &metav1.Condition{
Type: GitHubConnectionValid,

View File

@@ -6,6 +6,7 @@ package githubupstreamwatcher
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"fmt"
"net"
@@ -368,13 +369,17 @@ func TestController(t *testing.T) {
name string
githubIdentityProviders []runtime.Object
secrets []runtime.Object
mockDialer func(network, addr string, config *tls.Config) (*tls.Conn, error)
wantErr string
wantLogs []string
wantResultingCache []*upstreamgithub.ProviderConfig
wantResultingUpstreams []v1alpha1.GitHubIdentityProvider
}{
{
name: "no GitHubIdentityProviders",
name: "no GitHubIdentityProviders",
wantResultingCache: []*upstreamgithub.ProviderConfig{},
wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{},
wantLogs: []string{},
},
{
name: "happy path with all fields",
@@ -386,17 +391,23 @@ func TestController(t *testing.T) {
{
Name: "some-idp-name",
ResourceUID: "some-resource-uid",
Host: fmt.Sprintf("https://%s", *validFilledOutIDP.Spec.GitHubAPI.Host),
APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host),
UsernameAttribute: "id",
GroupNameAttribute: "name",
OAuth2Config: &oauth2.Config{
ClientID: "some-client-id",
ClientSecret: "some-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
DeviceAuthURL: "", // not used
TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host),
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // not used
Scopes: []string{"read:user", "read:org"},
},
AllowedOrganizations: []string{"organization1", "org2"},
OrganizationLoginPolicy: "OnlyUsersFromAllowedOrganizations",
AuthorizationURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
HttpClient: nil, // let the test runner populate this for us
AllowedOrganizations: []string{"organization1", "org2"},
HttpClient: nil, // let the test runner populate this for us
},
},
wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{
@@ -436,16 +447,22 @@ func TestController(t *testing.T) {
{
Name: "minimal-idp-name",
ResourceUID: "minimal-uid",
Host: fmt.Sprintf("https://%s", goodServerDomain),
APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host),
UsernameAttribute: "login",
GroupNameAttribute: "slug",
OAuth2Config: &oauth2.Config{
ClientID: "some-client-id",
ClientSecret: "some-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
DeviceAuthURL: "", // not used
TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host),
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // not used
Scopes: []string{"read:user", "read:org"},
},
OrganizationLoginPolicy: "AllGitHubUsers",
AuthorizationURL: fmt.Sprintf("https://%s/login/oauth/authorize", goodServerDomain),
HttpClient: nil, // let the test runner populate this for us
HttpClient: nil, // let the test runner populate this for us
},
},
wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{
@@ -475,6 +492,80 @@ func TestController(t *testing.T) {
buildLogForUpdatingPhase("minimal-idp-name", "Ready"),
},
},
{
name: "happy path using github.com",
secrets: []runtime.Object{goodSecret},
githubIdentityProviders: []runtime.Object{
func() runtime.Object {
githubIDP := validMinimalIDP.DeepCopy()
githubIDP.Spec.GitHubAPI.Host = ptr.To("github.com")
// don't change the CA because we are not really going to dial github.com in this test
return githubIDP
}(),
},
mockDialer: func(network, addr string, config *tls.Config) (*tls.Conn, error) {
require.Equal(t, "github.com:443", addr)
// don't actually dial github.com to avoid making external network calls in unit test
certPool, _, err := pinnipedcontroller.BuildCertPoolIDP(validMinimalIDP.Spec.GitHubAPI.TLS)
require.NoError(t, err)
configClone := config.Clone()
configClone.RootCAs = certPool
return tls.Dial(network, goodServerDomain, configClone)
},
wantResultingCache: []*upstreamgithub.ProviderConfig{
{
Name: "minimal-idp-name",
ResourceUID: "minimal-uid",
APIBaseURL: "https://api.github.com",
UsernameAttribute: "login",
GroupNameAttribute: "slug",
OAuth2Config: &oauth2.Config{
ClientID: "some-client-id",
ClientSecret: "some-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: "https://github.com:443/login/oauth/authorize",
DeviceAuthURL: "", // not used
TokenURL: "https://github.com:443/login/oauth/access_token",
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // not used
Scopes: []string{"read:user", "read:org"},
},
HttpClient: nil, // let the test runner populate this for us
},
},
wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{
{
ObjectMeta: validMinimalIDP.ObjectMeta,
Spec: func() v1alpha1.GitHubIdentityProviderSpec {
githubIDP := validMinimalIDP.DeepCopy()
githubIDP.Spec.GitHubAPI.Host = ptr.To("github.com")
// don't change the CA because we are not really going to dial github.com in this test
return githubIDP.Spec
}(),
Status: v1alpha1.GitHubIdentityProviderStatus{
Phase: v1alpha1.GitHubPhaseReady,
Conditions: []metav1.Condition{
buildClaimsValidatedTrue(t),
buildClientCredentialsObtainedTrue(t, validMinimalIDP.Spec.Client.SecretName),
buildGitHubConnectionValidTrue(t, "github.com:443"),
buildHostValidTrue(t, "github.com"),
buildOrganizationsPolicyValidTrue(t, *validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy),
buildTLSConfigurationValidTrue(t),
},
},
},
},
wantLogs: []string{
buildLogForUpdatingClientCredentialsObtained("minimal-idp-name", "True", "Success", fmt.Sprintf(`clientID and clientSecret have been read from spec.client.SecretName (\"%s\")`, validMinimalIDP.Spec.Client.SecretName)),
buildLogForUpdatingClaimsValidTrue("minimal-idp-name"),
buildLogForUpdatingOrganizationPolicyValid("minimal-idp-name", "True", "Success", fmt.Sprintf(`spec.allowAuthentication.organizations.policy (\"%s\") is valid`, string(*validMinimalIDP.Spec.AllowAuthentication.Organizations.Policy))),
buildLogForUpdatingHostValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is valid`, "github.com"),
buildLogForUpdatingTLSConfigurationValid("minimal-idp-name", "True", "Success", "spec.githubAPI.tls.certificateAuthorityData is valid"),
buildLogForUpdatingGitHubConnectionValid("minimal-idp-name", "True", "Success", `spec.githubAPI.host (\"%s\") is reachable and TLS verification succeeds`, "github.com:443"),
buildLogForUpdatingPhase("minimal-idp-name", "Ready"),
},
},
{
name: "happy path with IPv6",
secrets: []runtime.Object{goodSecret},
@@ -492,16 +583,22 @@ func TestController(t *testing.T) {
{
Name: "minimal-idp-name",
ResourceUID: "minimal-uid",
Host: fmt.Sprintf("https://%s", goodServerIPv6Domain),
APIBaseURL: fmt.Sprintf("https://%s/api/v3", goodServerIPv6Domain),
UsernameAttribute: "login",
GroupNameAttribute: "slug",
OAuth2Config: &oauth2.Config{
ClientID: "some-client-id",
ClientSecret: "some-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", goodServerIPv6Domain),
DeviceAuthURL: "", // not used
TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", goodServerIPv6Domain),
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // not used
Scopes: []string{"read:user", "read:org"},
},
OrganizationLoginPolicy: "AllGitHubUsers",
AuthorizationURL: fmt.Sprintf("https://%s/login/oauth/authorize", goodServerIPv6Domain),
HttpClient: nil, // let the test runner populate this for us
HttpClient: nil, // let the test runner populate this for us
},
},
wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{
@@ -573,32 +670,44 @@ func TestController(t *testing.T) {
{
Name: "some-idp-name",
ResourceUID: "some-resource-uid",
Host: fmt.Sprintf("https://%s", *validFilledOutIDP.Spec.GitHubAPI.Host),
APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host),
UsernameAttribute: "id",
GroupNameAttribute: "name",
OAuth2Config: &oauth2.Config{
ClientID: "some-client-id",
ClientSecret: "some-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
DeviceAuthURL: "", // not used
TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host),
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // not used
Scopes: []string{"read:user", "read:org"},
},
AllowedOrganizations: []string{"organization1", "org2"},
OrganizationLoginPolicy: "OnlyUsersFromAllowedOrganizations",
AuthorizationURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
HttpClient: nil, // let the test runner populate this for us
AllowedOrganizations: []string{"organization1", "org2"},
HttpClient: nil, // let the test runner populate this for us
},
{
Name: "other-idp-name",
ResourceUID: "some-resource-uid",
Host: fmt.Sprintf("https://%s", *validFilledOutIDP.Spec.GitHubAPI.Host),
APIBaseURL: fmt.Sprintf("https://%s/api/v3", *validFilledOutIDP.Spec.GitHubAPI.Host),
UsernameAttribute: "login:id",
GroupNameAttribute: "name",
OAuth2Config: &oauth2.Config{
ClientID: "other-client-id",
ClientSecret: "other-client-secret",
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
DeviceAuthURL: "", // not used
TokenURL: fmt.Sprintf("https://%s/login/oauth/access_token", *validFilledOutIDP.Spec.GitHubAPI.Host),
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "", // not used
Scopes: []string{"read:user", "read:org"},
},
AllowedOrganizations: []string{"organization1", "org2"},
OrganizationLoginPolicy: "OnlyUsersFromAllowedOrganizations",
AuthorizationURL: fmt.Sprintf("https://%s/login/oauth/authorize", *validFilledOutIDP.Spec.GitHubAPI.Host),
HttpClient: nil, // let the test runner populate this for us
AllowedOrganizations: []string{"organization1", "org2"},
HttpClient: nil, // let the test runner populate this for us
},
},
wantResultingUpstreams: []v1alpha1.GitHubIdentityProvider{
@@ -1727,6 +1836,11 @@ func TestController(t *testing.T) {
gitHubIdentityProviderInformer := supervisorInformers.IDP().V1alpha1().GitHubIdentityProviders()
dialer := tt.mockDialer
if dialer == nil {
dialer = tls.Dial
}
controller := New(
namespace,
cache,
@@ -1736,6 +1850,7 @@ func TestController(t *testing.T) {
logger,
controllerlib.WithInformer,
frozenClock,
dialer,
)
ctx, cancel := context.WithCancel(context.Background())
@@ -1759,25 +1874,22 @@ func TestController(t *testing.T) {
require.Equal(t, len(tt.wantResultingCache), len(actualIDPList))
for i := 0; i < len(tt.wantResultingCache); i++ {
// Do not expect any particular order in the cache
var actualIDP *upstreamgithub.Provider
var actualProvider *upstreamgithub.Provider
for _, possibleIDP := range actualIDPList {
if possibleIDP.GetName() == tt.wantResultingCache[i].Name {
var ok bool
actualIDP, ok = possibleIDP.(*upstreamgithub.Provider)
actualProvider, ok = possibleIDP.(*upstreamgithub.Provider)
require.True(t, ok)
break
}
}
require.Equal(t, tt.wantResultingCache[i].Name, actualIDP.GetName())
require.Equal(t, tt.wantResultingCache[i].ResourceUID, actualIDP.GetResourceUID())
require.Equal(t, tt.wantResultingCache[i].Host, actualIDP.GetHost())
require.Equal(t, tt.wantResultingCache[i].OAuth2Config.ClientID, actualIDP.GetClientID())
require.Equal(t, tt.wantResultingCache[i].GroupNameAttribute, actualIDP.GetGroupNameAttribute())
require.Equal(t, tt.wantResultingCache[i].UsernameAttribute, actualIDP.GetUsernameAttribute())
require.Equal(t, tt.wantResultingCache[i].AllowedOrganizations, actualIDP.GetAllowedOrganizations())
require.Equal(t, tt.wantResultingCache[i].OrganizationLoginPolicy, actualIDP.GetOrganizationLoginPolicy())
require.Equal(t, tt.wantResultingCache[i].AuthorizationURL, actualIDP.GetAuthorizationURL())
require.Equal(t, tt.wantResultingCache[i].Name, actualProvider.GetName())
require.Equal(t, tt.wantResultingCache[i].ResourceUID, actualProvider.GetResourceUID())
require.Equal(t, tt.wantResultingCache[i].OAuth2Config.ClientID, actualProvider.GetClientID())
require.Equal(t, tt.wantResultingCache[i].GroupNameAttribute, actualProvider.GetGroupNameAttribute())
require.Equal(t, tt.wantResultingCache[i].UsernameAttribute, actualProvider.GetUsernameAttribute())
require.Equal(t, tt.wantResultingCache[i].AllowedOrganizations, actualProvider.GetAllowedOrganizations())
require.GreaterOrEqual(t, len(tt.githubIdentityProviders), i+1, "there must be at least as many input identity providers as items in the cache")
githubIDP, ok := tt.githubIdentityProviders[i].(*v1alpha1.GitHubIdentityProvider)
@@ -1785,8 +1897,9 @@ func TestController(t *testing.T) {
certPool, _, err := pinnipedcontroller.BuildCertPoolIDP(githubIDP.Spec.GitHubAPI.TLS)
require.NoError(t, err)
compareTLSClientConfigWithinHttpClients(t, phttp.Default(certPool), actualIDP.GetHttpClient())
require.Equal(t, tt.wantResultingCache[i].OAuth2Config, actualIDP.GetOAuth2Config())
compareTLSClientConfigWithinHttpClients(t, phttp.Default(certPool), actualProvider.GetConfig().HttpClient)
require.Equal(t, tt.wantResultingCache[i].OAuth2Config, actualProvider.GetConfig().OAuth2Config)
require.Contains(t, tt.wantResultingCache[i].APIBaseURL, actualProvider.GetConfig().APIBaseURL)
}
// Verify the status conditions as reported in Kubernetes
@@ -2120,6 +2233,7 @@ func TestController_OnlyWantActions(t *testing.T) {
logger,
controllerlib.WithInformer,
frozenClock,
tls.Dial,
)
ctx, cancel := context.WithCancel(context.Background())
@@ -2230,6 +2344,7 @@ func TestGitHubUpstreamWatcherControllerFilterSecret(t *testing.T) {
logger,
observableInformers.WithInformer,
clock.RealClock{},
tls.Dial,
)
unrelated := &corev1.Secret{}
@@ -2299,6 +2414,7 @@ func TestGitHubUpstreamWatcherControllerFilterGitHubIDP(t *testing.T) {
logger,
observableInformers.WithInformer,
clock.RealClock{},
tls.Dial,
)
unrelated := &v1alpha1.GitHubIdentityProvider{}

View File

@@ -19,7 +19,7 @@ import (
"go.pinniped.dev/pkg/oidcclient/pkce"
)
// FederationDomainResolvedGitHubIdentityProvider respresents a FederationDomainIdentityProvider which has
// FederationDomainResolvedGitHubIdentityProvider represents a FederationDomainIdentityProvider which has
// been resolved dynamically based on the currently loaded IDP CRs to include the provider.UpstreamGitHubIdentityProviderI
// and other metadata about the provider.
type FederationDomainResolvedGitHubIdentityProvider struct {
@@ -76,37 +76,33 @@ func (p *FederationDomainResolvedGitHubIdentityProvider) UpstreamAuthorizeRedire
AuthURL: p.Provider.GetAuthorizationURL(),
},
RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuerURL),
Scopes: p.Provider.GetScopes(),
}
redirectURL := upstreamOAuthConfig.AuthCodeURL(
state.EncodedStateParam,
)
redirectURL := upstreamOAuthConfig.AuthCodeURL(state.EncodedStateParam)
return redirectURL, nil
}
func (p *FederationDomainResolvedGitHubIdentityProvider) Login(
_ context.Context,
submittedUsername string,
submittedPassword string,
_ string,
_ string,
) (*resolvedprovider.Identity, *resolvedprovider.IdentityLoginExtras, error) {
fmt.Printf("GithubResolvedIdentityProvider ~ Login() called with submittedUserName %s, submittedPassword %s", submittedUsername, submittedPassword)
return nil, nil, errors.New("function Login not yet implemented for GitHub IDP")
}
func (p *FederationDomainResolvedGitHubIdentityProvider) LoginFromCallback(
_ context.Context,
authCode string,
pkce pkce.Code,
nonce nonce.Nonce,
redirectURI string,
_ string,
_ pkce.Code,
_ nonce.Nonce,
_ string,
) (*resolvedprovider.Identity, *resolvedprovider.IdentityLoginExtras, error) {
fmt.Printf("GithubResolvedIdentityProvider ~ LoginFromCallback() called with authCode: %s, pkce: %#v, nonce: %#v, redirectURI: %s", authCode, pkce, nonce, redirectURI)
return nil, nil, errors.New("function LoginFromCallback not yet implemented for GitHub IDP")
}
func (p *FederationDomainResolvedGitHubIdentityProvider) UpstreamRefresh(
_ context.Context,
identity *resolvedprovider.Identity,
_ *resolvedprovider.Identity,
) (refreshedIdentity *resolvedprovider.RefreshedIdentity, err error) {
fmt.Printf("GithubResolvedIdentityProvider ~ UpstreamRefresh() called with identity %#v", identity)
return nil, errors.New("function UpstreamRefresh not yet implemented for GitHub IDP")
}

View File

@@ -4,64 +4,80 @@
package resolvedgithub
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
idpdiscoveryv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idpdiscovery/v1alpha1"
"go.pinniped.dev/internal/federationdomain/resolvedprovider"
"go.pinniped.dev/internal/idtransform"
"go.pinniped.dev/internal/psession"
"go.pinniped.dev/internal/testutil/transformtestutil"
"go.pinniped.dev/internal/upstreamgithub"
)
type fakeTransformer struct{}
func (a fakeTransformer) Evaluate(_ context.Context, _ string, _ []string) (*idtransform.TransformationResult, error) {
return &idtransform.TransformationResult{}, nil
}
func (a fakeTransformer) Source() interface{} { return nil }
func TestFederationDomainResolvedGitHubIdentityProvider(t *testing.T) {
fake := fakeTransformer{}
transforms := idtransform.NewTransformationPipeline()
transforms.AppendTransformation(fake)
subject := FederationDomainResolvedGitHubIdentityProvider{
DisplayName: "fake-display-name",
Provider: upstreamgithub.New(upstreamgithub.ProviderConfig{
Name: "fake-provider-config",
ResourceUID: "fake-resource-uid",
OAuth2Config: &oauth2.Config{
ClientID: "clientID12345",
ClientSecret: "clientSecret6789",
RedirectURL: "some/redirect/url",
transforms := transformtestutil.NewRejectAllAuthPipeline(t)
provider := upstreamgithub.New(upstreamgithub.ProviderConfig{
Name: "fake-provider-config",
ResourceUID: "fake-resource-uid",
APIBaseURL: "https://fake-api-host.com",
UsernameAttribute: idpv1alpha1.GitHubUsernameID,
GroupNameAttribute: idpv1alpha1.GitHubUseTeamSlugForGroupName,
AllowedOrganizations: []string{"org1", "org2"},
HttpClient: nil, // not needed yet for this test
OAuth2Config: &oauth2.Config{
ClientID: "fake-client-id",
ClientSecret: "fake-client-secret",
Scopes: []string{"read:user", "read:org"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://fake-authorization-url",
DeviceAuthURL: "",
TokenURL: "https://fake-token-url",
AuthStyle: oauth2.AuthStyleInParams,
},
}),
},
})
subject := FederationDomainResolvedGitHubIdentityProvider{
DisplayName: "fake-display-name",
Provider: provider,
SessionProviderType: psession.ProviderTypeGitHub,
Transforms: transforms,
}
require.Equal(t, "fake-display-name", subject.GetDisplayName())
require.Equal(t, upstreamgithub.New(upstreamgithub.ProviderConfig{
Name: "fake-provider-config",
ResourceUID: "fake-resource-uid",
OAuth2Config: &oauth2.Config{
ClientID: "clientID12345",
ClientSecret: "clientSecret6789",
RedirectURL: "some/redirect/url",
},
}), subject.GetProvider())
require.Equal(t, provider, subject.GetProvider())
require.Equal(t, psession.ProviderTypeGitHub, subject.GetSessionProviderType())
require.Equal(t, v1alpha1.IDPTypeGitHub, subject.GetIDPDiscoveryType())
require.Equal(t, []v1alpha1.IDPFlow{v1alpha1.IDPFlowBrowserAuthcode}, subject.GetIDPDiscoveryFlows())
require.Equal(t, idpdiscoveryv1alpha1.IDPTypeGitHub, subject.GetIDPDiscoveryType())
require.Equal(t, []idpdiscoveryv1alpha1.IDPFlow{idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode}, subject.GetIDPDiscoveryFlows())
require.Equal(t, transforms, subject.GetTransforms())
require.Equal(t, &psession.GitHubSessionData{}, subject.CloneIDPSpecificSessionDataFromSession(&psession.CustomSessionData{
originalCustomSession := &psession.CustomSessionData{
Username: "fake-username",
UpstreamUsername: "fake-upstream-username",
GitHub: &psession.GitHubSessionData{},
}))
GitHub: &psession.GitHubSessionData{UpstreamAccessToken: "fake-upstream-access-token"},
}
clonedCustomSession := subject.CloneIDPSpecificSessionDataFromSession(originalCustomSession)
require.Equal(t,
&psession.GitHubSessionData{UpstreamAccessToken: "fake-upstream-access-token"},
clonedCustomSession,
)
require.NotSame(t, originalCustomSession, clonedCustomSession)
customSessionToBeMutated := &psession.CustomSessionData{
Username: "fake-username2",
UpstreamUsername: "fake-upstream-username2",
}
subject.ApplyIDPSpecificSessionDataToSession(customSessionToBeMutated, &psession.GitHubSessionData{UpstreamAccessToken: "fake-upstream-access-token2"})
require.Equal(t, &psession.CustomSessionData{
Username: "fake-username2",
UpstreamUsername: "fake-upstream-username2",
GitHub: &psession.GitHubSessionData{UpstreamAccessToken: "fake-upstream-access-token2"},
}, customSessionToBeMutated)
redirectURL, err := subject.UpstreamAuthorizeRedirectURL(
&resolvedprovider.UpstreamAuthorizeRequestState{
EncodedStateParam: "encodedStateParam12345",
@@ -72,7 +88,12 @@ func TestFederationDomainResolvedGitHubIdentityProvider(t *testing.T) {
)
require.NoError(t, err)
require.Equal(t,
"?client_id=clientID12345&redirect_uri=https%3A%2F%2Flocalhost%2Ffake%2Fpath%2Fcallback&response_type=code&state=encodedStateParam12345",
"https://fake-authorization-url?"+
"client_id=fake-client-id&"+
"redirect_uri=https%3A%2F%2Flocalhost%2Ffake%2Fpath%2Fcallback&"+
"response_type=code&"+
"scope=read%3Auser+read%3Aorg&"+
"state=encodedStateParam12345",
redirectURL,
)
}

View File

@@ -5,7 +5,6 @@ package upstreamprovider
import (
"context"
"net/http"
"net/url"
"golang.org/x/oauth2"
@@ -131,12 +130,12 @@ type UpstreamLDAPIdentityProviderI interface {
type UpstreamGithubIdentityProviderI interface {
UpstreamIdentityProviderI
// GetHost returns the hostname of the GitHub server. This is either "github.com" or a GitHub Enterprise Server.
GetHost() string
// GetClientID returns the OAuth client ID registered with the upstream provider to be used in the authorization code flow.
GetClientID() string
// GetScopes returns the scopes to request in authorization (authcode or password grant) flow.
GetScopes() []string
// GetUsernameAttribute returns the attribute from the GitHub API user response to use for the downstream username.
// See https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user.
// Note that this is a constructed value - do not expect that the result will exactly match one of the JSON fields.
@@ -147,23 +146,26 @@ type UpstreamGithubIdentityProviderI interface {
// Note that this is a constructed value - do not expect that the result will exactly match one of the JSON fields.
GetGroupNameAttribute() v1alpha1.GitHubGroupNameAttribute
// GetAllowedOrganizations returns a list of organizations configured to allow authentication. A user must have membership
// in at least one of these organizations to log in. Note that the user can specify a policy (returned by GetOrganizationLoginPolicy)
// to disregard organization membership for purposes of authentication.
//
// If this list is specified, only teams from the listed organizations should be represented as groups for the downstream token.
// GetAllowedOrganizations returns a list of organizations configured to allow authentication.
// If this list has contents, a user must have membership in at least one of these organizations to log in,
// and only teams from the listed organizations should be represented as groups for the downstream token.
// If this list is empty, then any user can log in regardless of org membership, and any observable
// teams memberships should be represented as groups for the downstream token.
GetAllowedOrganizations() []string
// GetOrganizationLoginPolicy must be "OnlyUsersFromAllowedOrganizations" if GetAllowedOrganizations has values.
// Otherwise, it must be "AllGitHubUsers", which means disregard the result of GetAllowedOrganizations.
GetOrganizationLoginPolicy() v1alpha1.GitHubAllowedAuthOrganizationsPolicy
// GetAuthorizationURL returns the authorization URL for the configured GitHub. This will look like:
// https://<spec.githubAPI.host>/login/oauth/authorize
// It will not include any query parameters or fragment. Any subdomains or port will come from <spec.githubAPI.host>.
// It will never include a username or password in the authority section.
GetAuthorizationURL() string
// GetHttpClient returns a http client configured with the provided CA bundle and a timeout.
GetHttpClient() *http.Client
// TODO: This interface should be easily mockable to avoid all interactions with the actual server.
// What interactions with the server do we want to hide behind this interface? Something like this?
// ExchangeAuthcode(ctx, authcode, redirectURI) (AccessToken, error)
// GetUser(ctx, accessToken) (User, error)
// GetUserOrgs(ctx, accessToken) ([]Org, error)
// GetUserTeams(ctx, accessToken) ([]Team, error)
// Or maybe higher level interface like this?
// ExchangeAuthcode(ctx, authcode, redirectURI) (AccessToken, error)
// GetUser(ctx, accessToken) (User, error) // in this case User would include team and org info
}

View File

@@ -383,15 +383,18 @@ const ExpectedAuthorizeCodeSessionJSONFromFuzzing = `{
"ȝƋ鬯犦獢9c5¤.岵": "浛a齙\\蹼偦歛"
}
},
"github": {}
"github": {
"upstreamAccessToken": " 皦pSǬŝ社Vƅȭǝ*擦28Dž"
}
}
},
"requestedAudience": [
"皦pSǬŝ社Vƅȭǝ*",
"Ƽĝ\"zvưã置bņ抰蛖a³"
"甍 ć\u003cʘ筫",
"蛖a³2ʫ承dʬ)ġ,TÀqy_"
],
"grantedAudience": [
"ʫ承dʬ)ġ,TÀqy_º"
"$+溪ŸȢŒų崓ļ憽",
"姧骦:駝重EȫʆɵʮGɃ"
]
},
"version": "7"

View File

@@ -145,6 +145,7 @@ func (s *ActiveDirectorySessionData) Clone() *ActiveDirectorySessionData {
}
type GitHubSessionData struct {
UpstreamAccessToken string `json:"upstreamAccessToken"`
}
func (s *GitHubSessionData) Clone() *GitHubSessionData {

View File

@@ -335,6 +335,7 @@ func prepareControllers(
plog.New(),
controllerlib.WithInformer,
clock.RealClock{},
tls.Dial,
),
singletonWorker).
WithController(

View File

@@ -4,8 +4,6 @@
package oidctestutil
import (
"net/http"
"k8s.io/apimachinery/pkg/types"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
@@ -15,16 +13,15 @@ import (
type TestUpstreamGitHubIdentityProviderBuilder struct {
name string
clientID string
resourceUID types.UID
clientID string
scopes []string
displayNameForFederationDomain string
transformsForFederationDomain *idtransform.TransformationPipeline
usernameAttribute v1alpha1.GitHubUsernameAttribute
groupNameAttribute v1alpha1.GitHubGroupNameAttribute
allowedOrganizations []string
organizationLoginPolicy v1alpha1.GitHubAllowedAuthOrganizationsPolicy
authorizationURL string
httpClient *http.Client
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) WithName(value string) *TestUpstreamGitHubIdentityProviderBuilder {
@@ -42,6 +39,11 @@ func (u *TestUpstreamGitHubIdentityProviderBuilder) WithClientID(value string) *
return u
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) WithScopes(value []string) *TestUpstreamGitHubIdentityProviderBuilder {
u.scopes = value
return u
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) WithDisplayNameForFederationDomain(value string) *TestUpstreamGitHubIdentityProviderBuilder {
u.displayNameForFederationDomain = value
return u
@@ -62,21 +64,11 @@ func (u *TestUpstreamGitHubIdentityProviderBuilder) WithAllowedOrganizations(val
return u
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) WithOrganizationLoginPolicy(value v1alpha1.GitHubAllowedAuthOrganizationsPolicy) *TestUpstreamGitHubIdentityProviderBuilder {
u.organizationLoginPolicy = value
return u
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) WithAuthorizationURL(value string) *TestUpstreamGitHubIdentityProviderBuilder {
u.authorizationURL = value
return u
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) WithHttpClient(value *http.Client) *TestUpstreamGitHubIdentityProviderBuilder {
u.httpClient = value
return u
}
func (u *TestUpstreamGitHubIdentityProviderBuilder) Build() *TestUpstreamGitHubIdentityProvider {
if u.displayNameForFederationDomain == "" {
// default it to the CR name
@@ -90,14 +82,13 @@ func (u *TestUpstreamGitHubIdentityProviderBuilder) Build() *TestUpstreamGitHubI
Name: u.name,
ResourceUID: u.resourceUID,
ClientID: u.clientID,
Scopes: u.scopes,
DisplayNameForFederationDomain: u.displayNameForFederationDomain,
TransformsForFederationDomain: u.transformsForFederationDomain,
UsernameAttribute: u.usernameAttribute,
GroupNameAttribute: u.groupNameAttribute,
AllowedOrganizations: u.allowedOrganizations,
OrganizationLoginPolicy: u.organizationLoginPolicy,
AuthorizationURL: u.authorizationURL,
HttpClient: u.httpClient,
}
}
@@ -109,15 +100,13 @@ type TestUpstreamGitHubIdentityProvider struct {
Name string
ClientID string
ResourceUID types.UID
Host string
Scopes []string
DisplayNameForFederationDomain string
TransformsForFederationDomain *idtransform.TransformationPipeline
UsernameAttribute v1alpha1.GitHubUsernameAttribute
GroupNameAttribute v1alpha1.GitHubGroupNameAttribute
AllowedOrganizations []string
OrganizationLoginPolicy v1alpha1.GitHubAllowedAuthOrganizationsPolicy
AuthorizationURL string
HttpClient *http.Client
}
var _ upstreamprovider.UpstreamGithubIdentityProviderI = &TestUpstreamGitHubIdentityProvider{}
@@ -130,8 +119,8 @@ func (u *TestUpstreamGitHubIdentityProvider) GetName() string {
return u.Name
}
func (u *TestUpstreamGitHubIdentityProvider) GetHost() string {
return u.Host
func (u *TestUpstreamGitHubIdentityProvider) GetScopes() []string {
return u.Scopes
}
func (u *TestUpstreamGitHubIdentityProvider) GetClientID() string {
@@ -150,14 +139,6 @@ func (u *TestUpstreamGitHubIdentityProvider) GetAllowedOrganizations() []string
return u.AllowedOrganizations
}
func (u *TestUpstreamGitHubIdentityProvider) GetOrganizationLoginPolicy() v1alpha1.GitHubAllowedAuthOrganizationsPolicy {
return u.OrganizationLoginPolicy
}
func (u *TestUpstreamGitHubIdentityProvider) GetAuthorizationURL() string {
return u.AuthorizationURL
}
func (u *TestUpstreamGitHubIdentityProvider) GetHttpClient() *http.Client {
return u.HttpClient
}

View File

@@ -16,16 +16,31 @@ import (
// ProviderConfig holds the active configuration of an upstream GitHub provider.
type ProviderConfig struct {
Name string
ResourceUID types.UID
Host string
UsernameAttribute v1alpha1.GitHubUsernameAttribute
GroupNameAttribute v1alpha1.GitHubGroupNameAttribute
OAuth2Config *oauth2.Config
AllowedOrganizations []string
OrganizationLoginPolicy v1alpha1.GitHubAllowedAuthOrganizationsPolicy
AuthorizationURL string
HttpClient *http.Client
Name string
ResourceUID types.UID
// APIBaseURL is the url of the GitHub API, not including the path to a specific API endpoint.
// According to the GitHub docs, it should be either https://api.github.com/ for cloud
// or https://HOSTNAME/api/v3/ for Enterprise Server.
APIBaseURL string
UsernameAttribute v1alpha1.GitHubUsernameAttribute
GroupNameAttribute v1alpha1.GitHubGroupNameAttribute
// AllowedOrganizations, when empty, means to allow users from all orgs.
AllowedOrganizations []string
// HttpClient is a client that can be used to call the GitHub APIs and token endpoint.
// This client should be configured with the user-provided CA bundle and a timeout.
HttpClient *http.Client
// OAuth2Config contains ClientID, ClientSecret, Scopes, and Endpoint (which contains auth and token endpoint URLs,
// and auth style for the token endpoint).
// OAuth2Config will not be used to compute the authorize URL because the redirect back to the Supervisor's
// callback must be different per FederationDomain. It holds data that may be useful when calculating the
// authorize URL, so that data is exposed by interface methods. However, it can be used to call the token endpoint,
// for which there is no RedirectURL needed.
OAuth2Config *oauth2.Config
}
type Provider struct {
@@ -40,12 +55,6 @@ func New(config ProviderConfig) *Provider {
return &Provider{c: config}
}
// GetConfig is a reader for the config. Returns a copy of the config to keep the underlying config read-only.
func (p *Provider) GetConfig() ProviderConfig {
return p.c
}
// GetName returns a name for this upstream provider.
func (p *Provider) GetName() string {
return p.c.Name
}
@@ -58,12 +67,8 @@ func (p *Provider) GetClientID() string {
return p.c.OAuth2Config.ClientID
}
func (p *Provider) GetOAuth2Config() *oauth2.Config {
return p.c.OAuth2Config
}
func (p *Provider) GetHost() string {
return p.c.Host
func (p *Provider) GetScopes() []string {
return p.c.OAuth2Config.Scopes
}
func (p *Provider) GetUsernameAttribute() v1alpha1.GitHubUsernameAttribute {
@@ -78,14 +83,11 @@ func (p *Provider) GetAllowedOrganizations() []string {
return p.c.AllowedOrganizations
}
func (p *Provider) GetOrganizationLoginPolicy() v1alpha1.GitHubAllowedAuthOrganizationsPolicy {
return p.c.OrganizationLoginPolicy
}
func (p *Provider) GetAuthorizationURL() string {
return p.c.AuthorizationURL
return p.c.OAuth2Config.Endpoint.AuthURL
}
func (p *Provider) GetHttpClient() *http.Client {
return p.c.HttpClient
// GetConfig returns the config. This is not part of the interface and is mostly just for testing.
func (p *Provider) GetConfig() ProviderConfig {
return p.c
}

View File

@@ -18,16 +18,21 @@ func TestGitHubProvider(t *testing.T) {
subject := New(ProviderConfig{
Name: "foo",
ResourceUID: "resource-uid-12345",
Host: "fake-host",
APIBaseURL: "https://fake-base-url",
UsernameAttribute: "fake-username-attribute",
GroupNameAttribute: "fake-group-name-attribute",
OAuth2Config: &oauth2.Config{
ClientID: "fake-client-id",
ClientSecret: "fake-client-secret",
Scopes: []string{"scope1", "scope2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://fake-authorization-url",
DeviceAuthURL: "",
TokenURL: "https://fake-token-url",
AuthStyle: oauth2.AuthStyleInParams,
},
},
AllowedOrganizations: []string{"fake-org", "fake-org2"},
OrganizationLoginPolicy: v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers,
AuthorizationURL: "https://fake-authorization-url",
AllowedOrganizations: []string{"fake-org", "fake-org2"},
HttpClient: &http.Client{
Timeout: 1234509,
},
@@ -36,16 +41,21 @@ func TestGitHubProvider(t *testing.T) {
require.Equal(t, ProviderConfig{
Name: "foo",
ResourceUID: "resource-uid-12345",
Host: "fake-host",
APIBaseURL: "https://fake-base-url",
UsernameAttribute: "fake-username-attribute",
GroupNameAttribute: "fake-group-name-attribute",
OAuth2Config: &oauth2.Config{
ClientID: "fake-client-id",
ClientSecret: "fake-client-secret",
Scopes: []string{"scope1", "scope2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://fake-authorization-url",
DeviceAuthURL: "",
TokenURL: "https://fake-token-url",
AuthStyle: oauth2.AuthStyleInParams,
},
},
AllowedOrganizations: []string{"fake-org", "fake-org2"},
OrganizationLoginPolicy: v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers,
AuthorizationURL: "https://fake-authorization-url",
AllowedOrganizations: []string{"fake-org", "fake-org2"},
HttpClient: &http.Client{
Timeout: 1234509,
},
@@ -54,17 +64,12 @@ func TestGitHubProvider(t *testing.T) {
require.Equal(t, "foo", subject.GetName())
require.Equal(t, types.UID("resource-uid-12345"), subject.GetResourceUID())
require.Equal(t, "fake-client-id", subject.GetClientID())
require.Equal(t, &oauth2.Config{
ClientID: "fake-client-id",
ClientSecret: "fake-client-secret",
}, subject.GetOAuth2Config())
require.Equal(t, "fake-host", subject.GetHost())
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, []string{"fake-org", "fake-org2"}, subject.GetAllowedOrganizations())
require.Equal(t, v1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers, subject.GetOrganizationLoginPolicy())
require.Equal(t, "https://fake-authorization-url", subject.GetAuthorizationURL())
require.Equal(t, &http.Client{
Timeout: 1234509,
}, subject.GetHttpClient())
}, subject.GetConfig().HttpClient)
}