forked from TrueCloudLab/distribution
Create authentication handler
Refactory authorizer to take a set of authentication handlers for different authentication schemes returned by an unauthorized HTTP requst. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
parent
7d630192dd
commit
ecaa643cb2
5 changed files with 143 additions and 126 deletions
|
@ -54,12 +54,12 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAuthHeader(header http.Header) []authorizationChallenge {
|
func parseAuthHeader(header http.Header) map[string]authorizationChallenge {
|
||||||
var challenges []authorizationChallenge
|
challenges := map[string]authorizationChallenge{}
|
||||||
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
||||||
v, p := parseValueAndParams(h)
|
v, p := parseValueAndParams(h)
|
||||||
if v != "" {
|
if v != "" {
|
||||||
challenges = append(challenges, authorizationChallenge{Scheme: v, Parameters: p})
|
challenges[v] = authorizationChallenge{Scheme: v, Parameters: p}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return challenges
|
return challenges
|
||||||
|
|
|
@ -13,25 +13,26 @@ func TestAuthChallengeParse(t *testing.T) {
|
||||||
if len(challenges) != 1 {
|
if len(challenges) != 1 {
|
||||||
t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges))
|
t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges))
|
||||||
}
|
}
|
||||||
|
challenge := challenges["bearer"]
|
||||||
|
|
||||||
if expected := "bearer"; challenges[0].Scheme != expected {
|
if expected := "bearer"; challenge.Scheme != expected {
|
||||||
t.Fatalf("Unexpected scheme: %s, expected: %s", challenges[0].Scheme, expected)
|
t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected := "https://auth.example.com/token"; challenges[0].Parameters["realm"] != expected {
|
if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected {
|
||||||
t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["realm"], expected)
|
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected := "registry.example.com"; challenges[0].Parameters["service"] != expected {
|
if expected := "registry.example.com"; challenge.Parameters["service"] != expected {
|
||||||
t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["service"], expected)
|
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected := "fun"; challenges[0].Parameters["other"] != expected {
|
if expected := "fun"; challenge.Parameters["other"] != expected {
|
||||||
t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["other"], expected)
|
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expected := "he\"llo"; challenges[0].Parameters["slashed"] != expected {
|
if expected := "he\"llo"; challenge.Parameters["slashed"] != expected {
|
||||||
t.Fatalf("Unexpected param: %s, expected: %s", challenges[0].Parameters["slashed"], expected)
|
t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,44 +6,9 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/docker/distribution/digest"
|
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
"github.com/docker/distribution/registry/api/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RepositoryNotFoundError is returned when making an operation against a
|
|
||||||
// repository that does not exist in the registry.
|
|
||||||
type RepositoryNotFoundError struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RepositoryNotFoundError) Error() string {
|
|
||||||
return fmt.Sprintf("No repository found with Name: %s", e.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageManifestNotFoundError is returned when making an operation against a
|
|
||||||
// given image manifest that does not exist in the registry.
|
|
||||||
type ImageManifestNotFoundError struct {
|
|
||||||
Name string
|
|
||||||
Tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ImageManifestNotFoundError) Error() string {
|
|
||||||
return fmt.Sprintf("No manifest found with Name: %s, Tag: %s",
|
|
||||||
e.Name, e.Tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobNotFoundError is returned when making an operation against a given image
|
|
||||||
// layer that does not exist in the registry.
|
|
||||||
type BlobNotFoundError struct {
|
|
||||||
Name string
|
|
||||||
Digest digest.Digest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *BlobNotFoundError) Error() string {
|
|
||||||
return fmt.Sprintf("No blob found with Name: %s, Digest: %s",
|
|
||||||
e.Name, e.Digest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobUploadNotFoundError is returned when making a blob upload operation against an
|
// BlobUploadNotFoundError is returned when making a blob upload operation against an
|
||||||
// invalid blob upload location url.
|
// invalid blob upload location url.
|
||||||
// This may be the result of using a cancelled, completed, or stale upload
|
// This may be the result of using a cancelled, completed, or stale upload
|
||||||
|
|
|
@ -48,7 +48,7 @@ func (hl *httpLayer) Read(p []byte) (n int, err error) {
|
||||||
n, err = rd.Read(p)
|
n, err = rd.Read(p)
|
||||||
hl.offset += int64(n)
|
hl.offset += int64(n)
|
||||||
|
|
||||||
// Simulate io.EOR error if we reach filesize.
|
// Simulate io.EOF error if we reach filesize.
|
||||||
if err == nil && hl.offset >= hl.size {
|
if err == nil && hl.offset >= hl.size {
|
||||||
err = io.EOF
|
err = io.EOF
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,13 @@ type Authorizer interface {
|
||||||
Authorize(req *http.Request) error
|
Authorize(req *http.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthenticationHandler is an interface for authorizing a request from
|
||||||
|
// params from a "WWW-Authenicate" header for a single scheme.
|
||||||
|
type AuthenticationHandler interface {
|
||||||
|
Scheme() string
|
||||||
|
AuthorizeRequest(req *http.Request, params map[string]string) error
|
||||||
|
}
|
||||||
|
|
||||||
// CredentialStore is an interface for getting credentials for
|
// CredentialStore is an interface for getting credentials for
|
||||||
// a given URL
|
// a given URL
|
||||||
type CredentialStore interface {
|
type CredentialStore interface {
|
||||||
|
@ -48,18 +55,6 @@ func (rc *RepositoryConfig) HTTPClient() (*http.Client, error) {
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenScope represents the scope at which a token will be requested.
|
|
||||||
// This represents a specific action on a registry resource.
|
|
||||||
type TokenScope struct {
|
|
||||||
Resource string
|
|
||||||
Scope string
|
|
||||||
Actions []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts TokenScope) String() string {
|
|
||||||
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTokenAuthorizer returns an authorizer which is capable of getting a token
|
// NewTokenAuthorizer returns an authorizer which is capable of getting a token
|
||||||
// from a token server. The expected authorization method will be discovered
|
// from a token server. The expected authorization method will be discovered
|
||||||
// by the authorizer, getting the token server endpoint from the URL being
|
// by the authorizer, getting the token server endpoint from the URL being
|
||||||
|
@ -69,24 +64,37 @@ func (ts TokenScope) String() string {
|
||||||
func NewTokenAuthorizer(creds CredentialStore, header http.Header, scope TokenScope) Authorizer {
|
func NewTokenAuthorizer(creds CredentialStore, header http.Header, scope TokenScope) Authorizer {
|
||||||
return &tokenAuthorizer{
|
return &tokenAuthorizer{
|
||||||
header: header,
|
header: header,
|
||||||
creds: creds,
|
challenges: map[string]map[string]authorizationChallenge{},
|
||||||
scope: scope,
|
handlers: []AuthenticationHandler{
|
||||||
challenges: map[string][]authorizationChallenge{},
|
NewTokenHandler(creds, scope, header),
|
||||||
|
NewBasicHandler(creds),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthorizer creates an authorizer which can handle multiple authentication
|
||||||
|
// schemes. The handlers are tried in order, the higher priority authentication
|
||||||
|
// methods should be first.
|
||||||
|
func NewAuthorizer(header http.Header, handlers ...AuthenticationHandler) Authorizer {
|
||||||
|
return &tokenAuthorizer{
|
||||||
|
header: header,
|
||||||
|
challenges: map[string]map[string]authorizationChallenge{},
|
||||||
|
handlers: handlers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenAuthorizer struct {
|
type tokenAuthorizer struct {
|
||||||
header http.Header
|
header http.Header
|
||||||
challenges map[string][]authorizationChallenge
|
challenges map[string]map[string]authorizationChallenge
|
||||||
creds CredentialStore
|
handlers []AuthenticationHandler
|
||||||
scope TokenScope
|
|
||||||
|
|
||||||
tokenLock sync.Mutex
|
|
||||||
tokenCache string
|
|
||||||
tokenExpiration time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ta *tokenAuthorizer) ping(endpoint string) ([]authorizationChallenge, error) {
|
func (ta *tokenAuthorizer) client() *http.Client {
|
||||||
|
// TODO(dmcgowan): Use same transport which has properly configured TLS
|
||||||
|
return &http.Client{Transport: &Transport{ExtraHeader: ta.header}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *tokenAuthorizer) ping(endpoint string) (map[string]authorizationChallenge, error) {
|
||||||
req, err := http.NewRequest("GET", endpoint, nil)
|
req, err := http.NewRequest("GET", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -98,6 +106,7 @@ func (ta *tokenAuthorizer) ping(endpoint string) ([]authorizationChallenge, erro
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// TODO(dmcgowan): Add version string which would allow skipping this section
|
||||||
var supportsV2 bool
|
var supportsV2 bool
|
||||||
HeaderLoop:
|
HeaderLoop:
|
||||||
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
|
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey("Docker-Distribution-API-Version")] {
|
||||||
|
@ -148,59 +157,80 @@ func (ta *tokenAuthorizer) Authorize(req *http.Request) error {
|
||||||
ta.challenges[pingEndpoint] = challenges
|
ta.challenges[pingEndpoint] = challenges
|
||||||
}
|
}
|
||||||
|
|
||||||
return ta.setAuth(challenges, req)
|
for _, handler := range ta.handlers {
|
||||||
}
|
challenge, ok := challenges[handler.Scheme()]
|
||||||
|
if ok {
|
||||||
func (ta *tokenAuthorizer) client() *http.Client {
|
if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
|
||||||
// TODO(dmcgowan): Use same transport which has properly configured TLS
|
|
||||||
return &http.Client{Transport: &Transport{ExtraHeader: ta.header}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ta *tokenAuthorizer) setAuth(challenges []authorizationChallenge, req *http.Request) error {
|
|
||||||
var useBasic bool
|
|
||||||
for _, challenge := range challenges {
|
|
||||||
switch strings.ToLower(challenge.Scheme) {
|
|
||||||
case "basic":
|
|
||||||
useBasic = true
|
|
||||||
case "bearer":
|
|
||||||
if err := ta.refreshToken(challenge); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ta.tokenCache))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
//log.Infof("Unsupported auth scheme: %q", challenge.Scheme)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use basic when no token auth challenges found
|
|
||||||
if useBasic {
|
|
||||||
if ta.creds != nil {
|
|
||||||
username, password := ta.creds.Basic(req.URL)
|
|
||||||
if username != "" && password != "" {
|
|
||||||
req.SetBasicAuth(username, password)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.New("no basic auth credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ta *tokenAuthorizer) refreshToken(challenge authorizationChallenge) error {
|
type tokenHandler struct {
|
||||||
ta.tokenLock.Lock()
|
header http.Header
|
||||||
defer ta.tokenLock.Unlock()
|
creds CredentialStore
|
||||||
|
scope TokenScope
|
||||||
|
|
||||||
|
tokenLock sync.Mutex
|
||||||
|
tokenCache string
|
||||||
|
tokenExpiration time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenScope represents the scope at which a token will be requested.
|
||||||
|
// This represents a specific action on a registry resource.
|
||||||
|
type TokenScope struct {
|
||||||
|
Resource string
|
||||||
|
Scope string
|
||||||
|
Actions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenHandler creates a new AuthenicationHandler which supports
|
||||||
|
// fetching tokens from a remote token server.
|
||||||
|
func NewTokenHandler(creds CredentialStore, scope TokenScope, header http.Header) AuthenticationHandler {
|
||||||
|
return &tokenHandler{
|
||||||
|
header: header,
|
||||||
|
creds: creds,
|
||||||
|
scope: scope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TokenScope) String() string {
|
||||||
|
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *tokenHandler) client() *http.Client {
|
||||||
|
// TODO(dmcgowan): Use same transport which has properly configured TLS
|
||||||
|
return &http.Client{Transport: &Transport{ExtraHeader: ts.header}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *tokenHandler) Scheme() string {
|
||||||
|
return "bearer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||||
|
if err := ts.refreshToken(params); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.tokenCache))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *tokenHandler) refreshToken(params map[string]string) error {
|
||||||
|
ts.tokenLock.Lock()
|
||||||
|
defer ts.tokenLock.Unlock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if now.After(ta.tokenExpiration) {
|
if now.After(ts.tokenExpiration) {
|
||||||
token, err := ta.fetchToken(challenge)
|
token, err := ts.fetchToken(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ta.tokenCache = token
|
ts.tokenCache = token
|
||||||
ta.tokenExpiration = now.Add(time.Minute)
|
ts.tokenExpiration = now.Add(time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -210,26 +240,20 @@ type tokenResponse struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token string, err error) {
|
func (ts *tokenHandler) fetchToken(params map[string]string) (token string, err error) {
|
||||||
//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
|
//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
|
||||||
params := map[string]string{}
|
|
||||||
for k, v := range challenge.Parameters {
|
|
||||||
params[k] = v
|
|
||||||
}
|
|
||||||
params["scope"] = ta.scope.String()
|
|
||||||
|
|
||||||
realm, ok := params["realm"]
|
realm, ok := params["realm"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("no realm specified for token auth challenge")
|
return "", errors.New("no realm specified for token auth challenge")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(dmcgowan): Handle empty scheme
|
||||||
|
|
||||||
realmURL, err := url.Parse(realm)
|
realmURL, err := url.Parse(realm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
|
return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(dmcgowan): Handle empty scheme
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", realmURL.String(), nil)
|
req, err := http.NewRequest("GET", realmURL.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -237,7 +261,7 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s
|
||||||
|
|
||||||
reqParams := req.URL.Query()
|
reqParams := req.URL.Query()
|
||||||
service := params["service"]
|
service := params["service"]
|
||||||
scope := params["scope"]
|
scope := ts.scope.String()
|
||||||
|
|
||||||
if service != "" {
|
if service != "" {
|
||||||
reqParams.Add("service", service)
|
reqParams.Add("service", service)
|
||||||
|
@ -247,8 +271,8 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s
|
||||||
reqParams.Add("scope", scopeField)
|
reqParams.Add("scope", scopeField)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ta.creds != nil {
|
if ts.creds != nil {
|
||||||
username, password := ta.creds.Basic(realmURL)
|
username, password := ts.creds.Basic(realmURL)
|
||||||
if username != "" && password != "" {
|
if username != "" && password != "" {
|
||||||
reqParams.Add("account", username)
|
reqParams.Add("account", username)
|
||||||
req.SetBasicAuth(username, password)
|
req.SetBasicAuth(username, password)
|
||||||
|
@ -257,7 +281,7 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s
|
||||||
|
|
||||||
req.URL.RawQuery = reqParams.Encode()
|
req.URL.RawQuery = reqParams.Encode()
|
||||||
|
|
||||||
resp, err := ta.client().Do(req)
|
resp, err := ts.client().Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -280,3 +304,30 @@ func (ta *tokenAuthorizer) fetchToken(challenge authorizationChallenge) (token s
|
||||||
|
|
||||||
return tr.Token, nil
|
return tr.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type basicHandler struct {
|
||||||
|
creds CredentialStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBasicHandler creaters a new authentiation handler which adds
|
||||||
|
// basic authentication credentials to a request.
|
||||||
|
func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
|
||||||
|
return &basicHandler{
|
||||||
|
creds: creds,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*basicHandler) Scheme() string {
|
||||||
|
return "basic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||||
|
if bh.creds != nil {
|
||||||
|
username, password := bh.creds.Basic(req.URL)
|
||||||
|
if username != "" && password != "" {
|
||||||
|
req.SetBasicAuth(username, password)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("no basic auth credentials")
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue