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

Add flag for Oauth request timeout #707

Merged
merged 8 commits into from
Jul 27, 2018
Merged
Show file tree
Hide file tree
Changes from 6 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
31 changes: 19 additions & 12 deletions cmd/skipper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ const (
defaultTLSHandshakeTimeoutBackend = 60 * time.Second
defaultMaxIdleConnsBackend = 0

// OAuth2:
defaultOAuthTokeninfoTimeout = 2 * time.Second // Not sure if this makes sense

// generic:
addressUsage = "network address that skipper should listen on"
ignoreTrailingSlashUsage = "flag indicating to ignore trailing slashes in paths when routing"
Expand Down Expand Up @@ -128,10 +131,11 @@ const (
kubernetesNamespaceUsage = "watch only this namespace for ingresses"

// OAuth2:
oauthURLUsage = "OAuth2 URL for Innkeeper authentication"
oauthCredentialsDirUsage = "directory where oauth credentials are stored: client.json and user.json"
oauthScopeUsage = "the whitespace separated list of oauth scopes"
oauth2TokeninfoURLUsage = "sets the default tokeninfo URL to query information about an incoming OAuth2 token in oauth2Tokeninfo filters"
oauthURLUsage = "OAuth2 URL for Innkeeper authentication"
oauthCredentialsDirUsage = "directory where oauth credentials are stored: client.json and user.json"
oauthScopeUsage = "the whitespace separated list of oauth scopes"
oauth2TokeninfoURLUsage = "sets the default tokeninfo URL to query information about an incoming OAuth2 token in oauth2Tokeninfo filters"
oauth2TokeninfoTimeoutUsage = "sets the default tokeninfo request timeout duration to 1500ms"

// connections, timeouts:
idleConnsPerHostUsage = "maximum idle connections per backend host"
Expand Down Expand Up @@ -235,10 +239,11 @@ var (
kubernetesNamespace string

// OAuth2:
oauthURL string
oauthScope string
oauthCredentialsDir string
oauth2TokeninfoURL string
oauthURL string
oauthScope string
oauthCredentialsDir string
oauth2TokeninfoURL string
oauth2TokeninfoTimeout time.Duration

// connections, timeouts:
idleConnsPerHost int
Expand Down Expand Up @@ -344,6 +349,7 @@ func init() {
flag.StringVar(&oauthScope, "oauth-scope", "", oauthScopeUsage)
flag.StringVar(&oauthCredentialsDir, "oauth-credentials-dir", "", oauthCredentialsDirUsage)
flag.StringVar(&oauth2TokeninfoURL, "oauth2-tokeninfo-url", "", oauth2TokeninfoURLUsage)
flag.DurationVar(&oauth2TokeninfoTimeout, "oauth2-tokeninfo-timeout", defaultOAuthTokeninfoTimeout, oauth2TokeninfoTimeoutUsage)

// connections, timeouts:
flag.IntVar(&idleConnsPerHost, "idle-conns-num", proxy.DefaultIdleConnsPerHost, idleConnsPerHostUsage)
Expand Down Expand Up @@ -499,10 +505,11 @@ func main() {
KubernetesNamespace: kubernetesNamespace,

// OAuth2:
OAuthUrl: oauthURL,
OAuthScope: oauthScope,
OAuthCredentialsDir: oauthCredentialsDir,
OAuthTokeninfoURL: oauth2TokeninfoURL,
OAuthUrl: oauthURL,
OAuthScope: oauthScope,
OAuthCredentialsDir: oauthCredentialsDir,
OAuthTokeninfoURL: oauth2TokeninfoURL,
OAuthTokeninfoTimeout: oauth2TokeninfoTimeout,

// connections, timeouts:
IdleConnectionsPerHost: idleConnsPerHost,
Expand Down
2 changes: 1 addition & 1 deletion filters/auth/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ the key value pairs match. Here "uid" has to have the value "jdoe" and
"foo" has to have the value "bar". Additionally the second will
check if there is a "realm" "/employees":

a: Path("/") -> oauthTokeninfoAllKV("uid", jdoe", "foo", "bar") -> "https://internal.example.org/";
a: Path("/") -> oauthTokeninfoAllKV("uid", "jdoe", "foo", "bar") -> "https://internal.example.org/";
b: Path("/") -> oauthTokeninfoAllKV("realm", "/employees", "uid", "jdoe", "foo", "bar") -> "https://internal.example.org/";

Example json output of this information response:
Expand Down
77 changes: 59 additions & 18 deletions filters/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"

log "github.com/sirupsen/logrus"
"github.com/zalando/skipper/filters"
Expand Down Expand Up @@ -49,13 +51,16 @@ const (

type (
authClient struct {
url *url.URL
url *url.URL
client *http.Client
quit chan struct{}
}

tokeninfoSpec struct {
typ roleCheckType
tokeninfoURL string
authClient *authClient
typ roleCheckType
tokeninfoURL string
tokenInfoTimeout time.Duration
authClient *authClient
}

filter struct {
Expand Down Expand Up @@ -145,9 +150,32 @@ func intersect(left []string, right []string) bool {
return false
}

func createHTTPClient(timeout time.Duration, quit chan struct{}) (*http.Client, error) {
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: timeout,
}).DialContext,
}

go func() {
for {
select {
case <-time.After(10 * time.Second):
transport.CloseIdleConnections()
case <-quit:
return
}
}
}()

return &http.Client{
Transport: transport,
}, nil
}

// jsonGet requests url with access token in the URL query specified
// by accessTokenQueryKey, if auth was given and writes into doc.
func jsonGet(url *url.URL, auth string, doc interface{}) error {
func jsonGet(url *url.URL, auth string, doc interface{}, client *http.Client) error {
if auth != "" {
q := url.Query()
q.Set(accessTokenQueryKey, auth)
Expand All @@ -159,7 +187,7 @@ func jsonGet(url *url.URL, auth string, doc interface{}) error {
return err
}

rsp, err := http.DefaultClient.Do(req)
rsp, err := client.Do(req)
if err != nil {
return err
}
Expand All @@ -173,50 +201,56 @@ func jsonGet(url *url.URL, auth string, doc interface{}) error {
return d.Decode(doc)
}

func newAuthClient(baseURL string) (*authClient, error) {
func newAuthClient(baseURL string, timeout time.Duration) (*authClient, error) {
u, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
return &authClient{url: u}, nil

quit := make(chan struct{})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to call close(quit), I would the following:

  1. add a Close() method to the filterspec
  2. call close(spec.authClient.quit) from this Close() method
  3. for normal skipper operation do nothing, do not call close, because there's going to be only a single instance of the spec
  4. for tests, always call Close() on the spec
  5. document that in case someone uses filter spec as a library in a custom build, they need to call Close() if they may create an arbitrary number of spec instances

@addityasingh @szuecs what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach looks clean to me, since we wouldn't need to pass down quit channel all the way down from skipper instance creation. And since the Close() is on filterspec, we wouldn't need to bother about the leaking goroutine as well. @szuecs Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the FilterSpec interface doesn't have the Close method, this approach wouldn't work. Check this https://play.golang.org/p/LkpQRoatvvn

Copy link
Member

@szuecs szuecs Jul 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to type cast it. Keep in mind if you need access from another package you have to export the type.

See: https://play.golang.org/p/BK0kM25jnjb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works in the playground, but not in the skipper test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you found out. An interface can only be casted to, if you use the pointer IIRC.

client, err := createHTTPClient(timeout, quit)
if err != nil {
log.Error("Unable to create http client")
}
return &authClient{url: u, client: client, quit: quit}, nil
}

func (ac *authClient) getTokeninfo(token string) (map[string]interface{}, error) {
var a map[string]interface{}
err := jsonGet(ac.url, token, &a)
err := jsonGet(ac.url, token, &a, ac.client)
return a, err
}

// NewOAuthTokeninfoAllScope creates a new auth filter specification
// to validate authorization for requests. Current implementation uses
// Bearer tokens to authorize requests and checks that the token
// contains all scopes.
func NewOAuthTokeninfoAllScope(OAuthTokeninfoURL string) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAllScopes, tokeninfoURL: OAuthTokeninfoURL}
func NewOAuthTokeninfoAllScope(OAuthTokeninfoURL string, OAuthTokeninfoTimeout time.Duration) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAllScopes, tokeninfoURL: OAuthTokeninfoURL, tokenInfoTimeout: OAuthTokeninfoTimeout}
}

// NewOAuthTokeninfoAnyScope creates a new auth filter specification
// to validate authorization for requests. Current implementation uses
// Bearer tokens to authorize requests and checks that the token
// contains at least one scope.
func NewOAuthTokeninfoAnyScope(OAuthTokeninfoURL string) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAnyScopes, tokeninfoURL: OAuthTokeninfoURL}
func NewOAuthTokeninfoAnyScope(OAuthTokeninfoURL string, OAuthTokeninfoTimeout time.Duration) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAnyScopes, tokeninfoURL: OAuthTokeninfoURL, tokenInfoTimeout: OAuthTokeninfoTimeout}
}

// NewOAuthTokeninfoAllKV creates a new auth filter specification
// to validate authorization for requests. Current implementation uses
// Bearer tokens to authorize requests and checks that the token
// contains all key value pairs provided.
func NewOAuthTokeninfoAllKV(OAuthTokeninfoURL string) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAllKV, tokeninfoURL: OAuthTokeninfoURL}
func NewOAuthTokeninfoAllKV(OAuthTokeninfoURL string, OAuthTokeninfoTimeout time.Duration) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAllKV, tokeninfoURL: OAuthTokeninfoURL, tokenInfoTimeout: OAuthTokeninfoTimeout}
}

// NewOAuthTokeninfoAnyKV creates a new auth filter specification
// to validate authorization for requests. Current implementation uses
// Bearer tokens to authorize requests and checks that the token
// contains at least one key value pair provided.
func NewOAuthTokeninfoAnyKV(OAuthTokeninfoURL string) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAnyKV, tokeninfoURL: OAuthTokeninfoURL}
func NewOAuthTokeninfoAnyKV(OAuthTokeninfoURL string, OAuthTokeninfoTimeout time.Duration) filters.Spec {
return &tokeninfoSpec{typ: checkOAuthTokeninfoAnyKV, tokeninfoURL: OAuthTokeninfoURL, tokenInfoTimeout: OAuthTokeninfoTimeout}
}

func (s *tokeninfoSpec) Name() string {
Expand Down Expand Up @@ -251,7 +285,7 @@ func (s *tokeninfoSpec) CreateFilter(args []interface{}) (filters.Filter, error)
return nil, filters.ErrInvalidFilterParameters
}

ac, err := newAuthClient(s.tokeninfoURL)
ac, err := newAuthClient(s.tokeninfoURL, s.tokenInfoTimeout)
if err != nil {
return nil, filters.ErrInvalidFilterParameters
}
Expand Down Expand Up @@ -280,6 +314,13 @@ func (s *tokeninfoSpec) CreateFilter(args []interface{}) (filters.Filter, error)
return f, nil
}

// Close cleans-up the quit channel used for this spec
func (s *tokeninfoSpec) Close() {
if s.authClient.quit != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A spec can create filters.
I think you have to close it from the filter, that's why you get a nil ptr panic.
See https://github.com/zalando/skipper/pull/707/files#diff-82513d1c973748c3c6b7fa0677fd3fd4L259, which shows that you store it in the filter and not in the spec.

close(s.authClient.quit)
}
}

func (kv kv) String() string {
var res []string
for k, v := range kv {
Expand Down
Loading