From 7f2f3a603ae018b1cd1c6a282104f44cd9a1a1d1 Mon Sep 17 00:00:00 2001 From: tiwood Date: Mon, 7 Mar 2022 21:55:40 +0100 Subject: [PATCH] Initial implementation --- core/endpoint.go | 39 ++++++ core/endpoint_test.go | 3 + security/endpoint_oidc.go | 13 ++ .../clientcredentials/clientcredentials.go | 120 ++++++++++++++++++ vendor/modules.txt | 1 + 5 files changed, 176 insertions(+) create mode 100644 security/endpoint_oidc.go create mode 100644 vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go diff --git a/core/endpoint.go b/core/endpoint.go index 1b908ae2..717f945e 100644 --- a/core/endpoint.go +++ b/core/endpoint.go @@ -2,9 +2,11 @@ package core import ( "bytes" + "context" "crypto/x509" "encoding/json" "errors" + "fmt" "io" "net" "net/http" @@ -15,7 +17,10 @@ import ( "github.com/TwiN/gatus/v3/alerting/alert" "github.com/TwiN/gatus/v3/client" "github.com/TwiN/gatus/v3/core/ui" + "github.com/TwiN/gatus/v3/security" "github.com/TwiN/gatus/v3/util" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" ) const ( @@ -44,6 +49,9 @@ var ( // ErrEndpointWithInvalidNameOrGroup is the error with which Gatus will panic if an endpoint has an invalid character where it shouldn't ErrEndpointWithInvalidNameOrGroup = errors.New("endpoint name and group must not have \" or \\") + + // ErrEndpointWithInvalidOIDCConfig is the error with which Gatus will panic if OIDC parameters are missing + ErrEndpointWithInvalidOIDCConfig = errors.New("issuer url, client id, client secret and scopes are required properties for endpoint oidc configuration") ) // Endpoint is the configuration of a monitored @@ -90,6 +98,9 @@ type Endpoint struct { // UIConfig is the configuration for the UI UIConfig *ui.Config `yaml:"ui,omitempty"` + // OIDCConfig is the configuration for obtaining an OIDC token for the endpoint + OIDCConfig *security.EndpointOIDCConfig `yaml:"oidc,omitempty"` + // NumberOfFailuresInARow is the number of unsuccessful evaluations in a row NumberOfFailuresInARow int `yaml:"-"` @@ -105,6 +116,11 @@ func (endpoint Endpoint) IsEnabled() bool { return *endpoint.Enabled } +// HasOIDCConfig return whether the endpoint has a OIDC configuration or not +func (endpoint Endpoint) HasOIDCConfig() bool { + return endpoint.OIDCConfig != nil +} + // ValidateAndSetDefaults validates the endpoint's configuration and sets the default value of fields that have one func (endpoint *Endpoint) ValidateAndSetDefaults() error { // Set default values @@ -154,6 +170,9 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error { if endpoint.DNS != nil { return endpoint.DNS.validateAndSetDefault() } + if endpoint.HasOIDCConfig() && !endpoint.OIDCConfig.IsValid() { + return ErrEndpointWithInvalidOIDCConfig + } // Make sure that the request can be created _, err := http.NewRequest(endpoint.Method, endpoint.URL, bytes.NewBuffer([]byte(endpoint.Body))) if err != nil { @@ -200,6 +219,17 @@ func (endpoint *Endpoint) EvaluateHealth() *Result { return result } +func (endpoint *Endpoint) getToken() (*oauth2.Token, error) { + c := clientcredentials.Config{ + ClientID: endpoint.OIDCConfig.ClientID, + ClientSecret: endpoint.OIDCConfig.ClientSecret, + Scopes: endpoint.OIDCConfig.Scopes, + TokenURL: endpoint.OIDCConfig.IssuerURL, + } + token, err := c.Token(context.Background()) + return token, err +} + func (endpoint *Endpoint) getIP(result *Result) { if endpoint.DNS != nil { result.Hostname = strings.TrimSuffix(endpoint.URL, ":53") @@ -231,6 +261,15 @@ func (endpoint *Endpoint) call(result *Result) { isTypeTLS := strings.HasPrefix(endpoint.URL, "tls://") isTypeHTTP := !isTypeDNS && !isTypeTCP && !isTypeICMP && !isTypeSTARTTLS && !isTypeTLS if isTypeHTTP { + if endpoint.HasOIDCConfig() { + token, err := endpoint.getToken() + if err != nil { + result.AddError(err.Error()) + return + } + authHeader := fmt.Sprintf("Bearer %s", token.AccessToken) + endpoint.Headers["Authorization"] = authHeader + } request = endpoint.buildHTTPRequest() } startTime := time.Now() diff --git a/core/endpoint_test.go b/core/endpoint_test.go index 77b90040..e2b620d4 100644 --- a/core/endpoint_test.go +++ b/core/endpoint_test.go @@ -65,6 +65,9 @@ func TestEndpoint_ValidateAndSetDefaults(t *testing.T) { if endpoint.Alerts[0].FailureThreshold != 3 { t.Error("Endpoint alert should've defaulted to a failure threshold of 3") } + if endpoint.HasOIDCConfig() { + t.Error("Endpoint OIDC config should've defaulted to 'nil'") + } } func TestEndpoint_ValidateAndSetDefaultsWithClientConfig(t *testing.T) { diff --git a/security/endpoint_oidc.go b/security/endpoint_oidc.go new file mode 100644 index 00000000..f6270dd1 --- /dev/null +++ b/security/endpoint_oidc.go @@ -0,0 +1,13 @@ +package security + +// EndpointOIDCConfig is the configuration for endpoint OIDC authentication +type EndpointOIDCConfig struct { + IssuerURL string `yaml:"issuer-url"` // e.g. https://dev-12345678.okta.com + ClientID string `yaml:"client-id"` + ClientSecret string `yaml:"client-secret"` + Scopes []string `yaml:"scopes"` // e.g. ["openid"] +} + +func (c *EndpointOIDCConfig) IsValid() bool { + return len(c.IssuerURL) > 0 && len(c.ClientID) > 0 && len(c.ClientSecret) > 0 && len(c.Scopes) > 0 +} diff --git a/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go new file mode 100644 index 00000000..7a0b9ed1 --- /dev/null +++ b/vendor/golang.org/x/oauth2/clientcredentials/clientcredentials.go @@ -0,0 +1,120 @@ +// Copyright 2014 The Go 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 clientcredentials implements the OAuth2.0 "client credentials" token flow, +// also known as the "two-legged OAuth 2.0". +// +// This should be used when the client is acting on its own behalf or when the client +// is the resource owner. It may also be used when requesting access to protected +// resources based on an authorization previously arranged with the authorization +// server. +// +// See https://tools.ietf.org/html/rfc6749#section-4.4 +package clientcredentials // import "golang.org/x/oauth2/clientcredentials" + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/internal" +) + +// Config describes a 2-legged OAuth2 flow, with both the +// client application information and the server's endpoint URLs. +type Config struct { + // ClientID is the application's ID. + ClientID string + + // ClientSecret is the application's secret. + ClientSecret string + + // TokenURL is the resource server's token endpoint + // URL. This is a constant specific to each server. + TokenURL string + + // Scope specifies optional requested permissions. + Scopes []string + + // EndpointParams specifies additional parameters for requests to the token endpoint. + EndpointParams url.Values + + // AuthStyle optionally specifies how the endpoint wants the + // client ID & client secret sent. The zero value means to + // auto-detect. + AuthStyle oauth2.AuthStyle +} + +// Token uses client credentials to retrieve a token. +// +// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable. +func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) { + return c.TokenSource(ctx).Token() +} + +// Client returns an HTTP client using the provided token. +// The token will auto-refresh as necessary. +// +// The provided context optionally controls which HTTP client +// is returned. See the oauth2.HTTPClient variable. +// +// The returned Client and its Transport should not be modified. +func (c *Config) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) +} + +// TokenSource returns a TokenSource that returns t until t expires, +// automatically refreshing it as necessary using the provided context and the +// client ID and client secret. +// +// Most users will use Config.Client instead. +func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { + source := &tokenSource{ + ctx: ctx, + conf: c, + } + return oauth2.ReuseTokenSource(nil, source) +} + +type tokenSource struct { + ctx context.Context + conf *Config +} + +// Token refreshes the token by using a new client credentials request. +// tokens received this way do not include a refresh token +func (c *tokenSource) Token() (*oauth2.Token, error) { + v := url.Values{ + "grant_type": {"client_credentials"}, + } + if len(c.conf.Scopes) > 0 { + v.Set("scope", strings.Join(c.conf.Scopes, " ")) + } + for k, p := range c.conf.EndpointParams { + // Allow grant_type to be overridden to allow interoperability with + // non-compliant implementations. + if _, ok := v[k]; ok && k != "grant_type" { + return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k) + } + v[k] = p + } + + tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle)) + if err != nil { + if rErr, ok := err.(*internal.RetrieveError); ok { + return nil, (*oauth2.RetrieveError)(rErr) + } + return nil, err + } + t := &oauth2.Token{ + AccessToken: tk.AccessToken, + TokenType: tk.TokenType, + RefreshToken: tk.RefreshToken, + Expiry: tk.Expiry, + } + return t.WithExtra(tk.Raw), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ef9af5b2..78594b4d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -116,6 +116,7 @@ golang.org/x/net/ipv6 # golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c ## explicit; go 1.11 golang.org/x/oauth2 +golang.org/x/oauth2/clientcredentials golang.org/x/oauth2/internal # golang.org/x/sync v0.0.0-20210220032951-036812b2e83c ## explicit