Add options struct to initialize handler

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
Derek McGowan 2016-03-04 11:32:48 -08:00
parent c9880e6b05
commit 2ef7a872de
2 changed files with 101 additions and 51 deletions

View file

@ -113,27 +113,45 @@ type clock interface {
type tokenHandler struct { type tokenHandler struct {
header http.Header header http.Header
creds CredentialStore creds CredentialStore
scope tokenScope
transport http.RoundTripper transport http.RoundTripper
clock clock clock clock
forceOAuth bool
clientID string
scopes []Scope
tokenLock sync.Mutex tokenLock sync.Mutex
tokenCache string tokenCache string
tokenExpiration time.Time tokenExpiration time.Time
additionalScopes map[string]struct{}
} }
// tokenScope represents the scope at which a token will be requested. // Scope is a type which is serializable to a string
// This represents a specific action on a registry resource. // using the allow scope grammar.
type tokenScope struct { type Scope interface {
Resource string String() string
Scope string
Actions []string
} }
func (ts tokenScope) String() string { // RepositoryScope represents a token scope for access
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) // to a repository.
type RepositoryScope struct {
Repository string
Actions []string
}
// String returns the string representation of the repository
// using the scope grammar
func (rs RepositoryScope) String() string {
return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ","))
}
// TokenHandlerOptions is used to configure a new token handler
type TokenHandlerOptions struct {
Transport http.RoundTripper
Credentials CredentialStore
ForceOAuth bool
ClientID string
Scopes []Scope
} }
// An implementation of clock for providing real time data. // An implementation of clock for providing real time data.
@ -145,22 +163,32 @@ func (realClock) Now() time.Time { return time.Now() }
// NewTokenHandler creates a new AuthenicationHandler which supports // NewTokenHandler creates a new AuthenicationHandler which supports
// fetching tokens from a remote token server. // fetching tokens from a remote token server.
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
return newTokenHandler(transport, creds, realClock{}, scope, actions...) // Create options...
return NewTokenHandlerWithOptions(TokenHandlerOptions{
Transport: transport,
Credentials: creds,
Scopes: []Scope{
RepositoryScope{
Repository: scope,
Actions: actions,
},
},
})
} }
// newTokenHandler exposes the option to provide a clock to manipulate time in unit testing. // NewTokenHandlerWithOptions creates a new token handler using the provided
func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler { // options structure.
return &tokenHandler{ func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
transport: transport, handler := &tokenHandler{
creds: creds, transport: options.Transport,
clock: c, creds: options.Credentials,
scope: tokenScope{ forceOAuth: options.ForceOAuth,
Resource: "repository", clientID: options.ClientID,
Scope: scope, scopes: options.Scopes,
Actions: actions, clock: realClock{},
},
additionalScopes: map[string]struct{}{},
} }
return handler
} }
func (th *tokenHandler) client() *http.Client { func (th *tokenHandler) client() *http.Client {
@ -177,10 +205,9 @@ func (th *tokenHandler) Scheme() string {
func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
var additionalScopes []string var additionalScopes []string
if fromParam := req.URL.Query().Get("from"); fromParam != "" { if fromParam := req.URL.Query().Get("from"); fromParam != "" {
additionalScopes = append(additionalScopes, tokenScope{ additionalScopes = append(additionalScopes, RepositoryScope{
Resource: "repository", Repository: fromParam,
Scope: fromParam, Actions: []string{"pull"},
Actions: []string{"pull"},
}.String()) }.String())
} }
if err := th.refreshToken(params, additionalScopes...); err != nil { if err := th.refreshToken(params, additionalScopes...); err != nil {
@ -195,16 +222,19 @@ func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]st
func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error { func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error {
th.tokenLock.Lock() th.tokenLock.Lock()
defer th.tokenLock.Unlock() defer th.tokenLock.Unlock()
scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
for _, scope := range th.scopes {
scopes = append(scopes, scope.String())
}
var addedScopes bool var addedScopes bool
for _, scope := range additionalScopes { for _, scope := range additionalScopes {
if _, ok := th.additionalScopes[scope]; !ok { scopes = append(scopes, scope)
th.additionalScopes[scope] = struct{}{} addedScopes = true
addedScopes = true
}
} }
now := th.clock.Now() now := th.clock.Now()
if now.After(th.tokenExpiration) || addedScopes { if now.After(th.tokenExpiration) || addedScopes {
token, expiration, err := th.fetchToken(params) token, expiration, err := th.fetchToken(params, scopes)
if err != nil { if err != nil {
return err return err
} }
@ -232,8 +262,12 @@ func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, servic
form.Set("scope", strings.Join(scopes, " ")) form.Set("scope", strings.Join(scopes, " "))
form.Set("service", service) form.Set("service", service)
// TODO: Make this configurable clientID := th.clientID
form.Set("client_id", "docker") if clientID == "" {
// Use default client, this is a required field
clientID = "registry-client"
}
form.Set("client_id", clientID)
if refreshToken != "" { if refreshToken != "" {
form.Set("grant_type", "refresh_token") form.Set("grant_type", "refresh_token")
@ -369,7 +403,7 @@ func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string,
return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
} }
func (th *tokenHandler) fetchToken(params map[string]string) (token string, expiration time.Time, err error) { func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
realm, ok := params["realm"] realm, ok := params["realm"]
if !ok { if !ok {
return "", time.Time{}, errors.New("no realm specified for token auth challenge") return "", time.Time{}, errors.New("no realm specified for token auth challenge")
@ -383,22 +417,13 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token string, expi
service := params["service"] service := params["service"]
scopes := make([]string, 0, 1+len(th.additionalScopes))
if len(th.scope.Actions) > 0 {
scopes = append(scopes, th.scope.String())
}
for scope := range th.additionalScopes {
scopes = append(scopes, scope)
}
var refreshToken string var refreshToken string
if th.creds != nil { if th.creds != nil {
refreshToken = th.creds.RefreshToken(realmURL, service) refreshToken = th.creds.RefreshToken(realmURL, service)
} }
// TODO(dmcgowan): define parameter to force oauth with password if refreshToken != "" || th.forceOAuth {
if refreshToken != "" {
return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes) return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
} }

View file

@ -220,7 +220,7 @@ func TestEndpointAuthorizeRefreshToken(t *testing.T) {
Request: testutil.Request{ Request: testutil.Request{
Method: "POST", Method: "POST",
Route: "/token", Route: "/token",
Body: []byte(fmt.Sprintf("client_id=docker&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)), Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)),
}, },
Response: testutil.Response{ Response: testutil.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
@ -232,7 +232,7 @@ func TestEndpointAuthorizeRefreshToken(t *testing.T) {
Request: testutil.Request{ Request: testutil.Request{
Method: "POST", Method: "POST",
Route: "/token", Route: "/token",
Body: []byte(fmt.Sprintf("client_id=docker&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope2), service)), Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope2), service)),
}, },
Response: testutil.Response{ Response: testutil.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
@ -243,7 +243,7 @@ func TestEndpointAuthorizeRefreshToken(t *testing.T) {
Request: testutil.Request{ Request: testutil.Request{
Method: "POST", Method: "POST",
Route: "/token", Route: "/token",
Body: []byte(fmt.Sprintf("client_id=docker&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken2, url.QueryEscape(scope2), service)), Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken2, url.QueryEscape(scope2), service)),
}, },
Response: testutil.Response{ Response: testutil.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
@ -542,7 +542,19 @@ func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
clock := &fakeClock{current: time.Now()} clock := &fakeClock{current: time.Now()}
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds))) options := TokenHandlerOptions{
Transport: nil,
Credentials: creds,
Scopes: []Scope{
RepositoryScope{
Repository: repo,
Actions: []string{"pull", "push"},
},
},
}
tHandler := NewTokenHandlerWithOptions(options)
tHandler.(*tokenHandler).clock = clock
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, tHandler, NewBasicHandler(creds)))
client := &http.Client{Transport: transport1} client := &http.Client{Transport: transport1}
// First call should result in a token exchange // First call should result in a token exchange
@ -680,7 +692,20 @@ func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, newTokenHandler(nil, creds, clock, repo, "pull", "push"), NewBasicHandler(creds)))
options := TokenHandlerOptions{
Transport: nil,
Credentials: creds,
Scopes: []Scope{
RepositoryScope{
Repository: repo,
Actions: []string{"pull", "push"},
},
},
}
tHandler := NewTokenHandlerWithOptions(options)
tHandler.(*tokenHandler).clock = clock
transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, tHandler, NewBasicHandler(creds)))
client := &http.Client{Transport: transport1} client := &http.Client{Transport: transport1}
// First call should result in a token exchange // First call should result in a token exchange