Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce CheckEndpointConnector, implement for OpenShift connector. #2512

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ type RefreshConnector interface {
// changes since the token was last refreshed.
Refresh(ctx context.Context, s Scopes, identity Identity) (Identity, error)
}

// CheckEndpointConnector is a connector that can test its endpoints for availability.
type CheckEndpointConnector interface {
// CheckEndpoint is called when a client wants to check the availability of
// endpoints used by the connector. If no error is returned, the connector
// can be assumed to be working.
CheckEndpoint(ctx context.Context) error
}
89 changes: 68 additions & 21 deletions connector/openshift/openshift.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const (
usersURLPath = "/apis/user.openshift.io/v1/users/~"
)

// Config holds configuration options for OpenShift login
// Config holds configuration options for OpenShift login.
type Config struct {
Issuer string `json:"issuer"`
ClientID string `json:"clientID"`
Expand All @@ -37,23 +37,27 @@ type Config struct {
RootCA string `json:"rootCA"`
}

// Option allows overriding default values of the openshift connector.
type Option func(*openshiftConnector)

var (
_ connector.CallbackConnector = (*openshiftConnector)(nil)
_ connector.RefreshConnector = (*openshiftConnector)(nil)
)

type openshiftConnector struct {
apiURL string
redirectURI string
clientID string
clientSecret string
cancel context.CancelFunc
logger log.Logger
httpClient *http.Client
oauth2Config *oauth2.Config
insecureCA bool
rootCA string
groups []string
apiURL string
redirectURI string
clientID string
clientSecret string
cancel context.CancelFunc
logger log.Logger
httpClient *http.Client
oauth2Config *oauth2.Config
insecureCA bool
rootCA string
groups []string
endpointsHealthCheck bool
}

type user struct {
Expand All @@ -64,6 +68,23 @@ type user struct {
Groups []string `json:"groups" protobuf:"bytes,4,rep,name=groups"`
}

// WithHTTPClient will inject a http.Client for the OpenShift connector. This can be used to
// add custom client specific settings.
func WithHTTPClient(httpClient *http.Client) Option {
return func(o *openshiftConnector) {
o.httpClient = httpClient
}
}

// WithEndpointHealthChecks will enable health checks for OAuth endpoints during creation of the
// OpenShift connector. If the endpoints are not reachable at the time of creation, the connector
// will not be created and a respective error returned.
func WithEndpointHealthChecks() Option {
return func(o *openshiftConnector) {
o.endpointsHealthCheck = true
}
}

// Open returns a connector which can be used to login users through an upstream
// OpenShift OAuth2 provider.
func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
Expand All @@ -72,20 +93,21 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
return nil, fmt.Errorf("failed to create HTTP client: %w", err)
}

return c.OpenWithHTTPClient(id, logger, httpClient)
return c.OpenWithOptions(id, logger, WithHTTPClient(httpClient))
}

// OpenWithHTTPClient returns a connector which can be used to login users through an upstream
// OpenShift OAuth2 provider. It provides the ability to inject a http.Client.
func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
httpClient *http.Client,
// OpenWithOptions returns a connector which can be used to login users through an upstream
// OpenShift OAuth2 provider. It provides the ability to override or inject values based on
// the given options.
func (c *Config) OpenWithOptions(id string, logger log.Logger,
options ...Option,
) (conn connector.Connector, err error) {
ctx, cancel := context.WithCancel(context.Background())

wellKnownURL := strings.TrimSuffix(c.Issuer, "/") + wellKnownURLPath
req, err := http.NewRequest(http.MethodGet, wellKnownURL, nil)

openshiftConnector := openshiftConnector{
openshiftConnector := &openshiftConnector{
apiURL: c.Issuer,
cancel: cancel,
clientID: c.ClientID,
Expand All @@ -95,7 +117,10 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
redirectURI: c.RedirectURI,
rootCA: c.RootCA,
groups: c.Groups,
httpClient: httpClient,
}

for _, opt := range options {
opt(openshiftConnector)
}

var metadata struct {
Expand Down Expand Up @@ -126,6 +151,13 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
Scopes: []string{"user:info"},
RedirectURL: c.RedirectURI,
}

if openshiftConnector.endpointsHealthCheck {
if err := openshiftConnector.oauth2EndpointsHealthCheck(ctx); err != nil {
return nil, fmt.Errorf("failed healthcheck for openshift oauth endpoints %w", err)
}
}

return &openshiftConnector, nil
}

Expand Down Expand Up @@ -155,7 +187,7 @@ func (e *oauth2Error) Error() string {
return e.error + ": " + e.errorDescription
}

// HandleCallback parses the request and returns the user's identity
// HandleCallback parses the request and returns the user's identity.
func (c *openshiftConnector) HandleCallback(s connector.Scopes,
r *http.Request,
) (identity connector.Identity, err error) {
Expand Down Expand Up @@ -227,7 +259,7 @@ func (c *openshiftConnector) identity(ctx context.Context, s connector.Scopes,
return identity, nil
}

// user function returns the OpenShift user associated with the authenticated user
// user function returns the OpenShift user associated with the authenticated user.
func (c *openshiftConnector) user(ctx context.Context, client *http.Client) (u user, err error) {
url := c.apiURL + usersURLPath

Expand Down Expand Up @@ -257,6 +289,21 @@ func (c *openshiftConnector) user(ctx context.Context, client *http.Client) (u u
return u, err
}

func (c *openshiftConnector) oauth2EndpointsHealthCheck(ctx context.Context) error {
for _, endpoint := range []string{c.oauth2Config.Endpoint.AuthURL, c.oauth2Config.Endpoint.TokenURL} {
req, err := http.NewRequest(http.MethodHead, endpoint, nil)
if err != nil {
return err
}
resp, err := c.httpClient.Do(req.WithContext(ctx))
if err != nil {
return err
}
resp.Body.Close()
}
return nil
}

func validateAllowedGroups(userGroups, allowedGroups []string) bool {
matchingGroups := groups.Filter(userGroups, allowedGroups)

Expand Down
24 changes: 24 additions & 0 deletions connector/openshift/openshift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,30 @@ func TestRefreshIdentityFailure(t *testing.T) {
expectEquals(t, connector.Identity{}, identity)
}

func TestCheckEndpoint(t *testing.T) {
s := newTestServer(map[string]interface{}{
"/oauth/authorize": nil,
"/oauth/token": nil,
})
defer s.Close()

h, err := newHTTPClient(true, "")
expectNil(t, err)

oc := openshiftConnector{apiURL: s.URL, httpClient: h, oauth2Config: &oauth2.Config{
Endpoint: oauth2.Endpoint{
AuthURL: s.URL + "/oauth/authorize", TokenURL: s.URL + "/oauth/token",
},
}}
err = oc.oauth2EndpointsHealthCheck(context.Background())
expectNil(t, err)

s.Close()

err = oc.oauth2EndpointsHealthCheck(context.Background())
expectNotNil(t, err)
}

func newTestServer(responses map[string]interface{}) *httptest.Server {
var s *httptest.Server
s = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down