Use context for auth access controllers
The auth package has been updated to use "golang.org/x/net/context" for passing information between the application and the auth backend. AccessControllers should now set a "auth.user" context value to a AuthUser struct containing a single "Name" field for now with possible, optional, values in the future. The "silly" auth backend always sets the name to "silly", while the "token" auth backend will set the name to match the "subject" claim of the JWT. Docker-DCO-1.1-Signed-off-by: Josh Hawn <josh.hawn@docker.com> (github: jlhawn)
This commit is contained in:
parent
c1c7d3dabf
commit
2c3d738a05
12 changed files with 1134 additions and 31 deletions
44
auth/auth.go
44
auth/auth.go
|
@ -18,7 +18,7 @@
|
|||
// resource := auth.Resource{Type: "customerOrder", Name: orderNumber}
|
||||
// access := auth.Access{Resource: resource, Action: "update"}
|
||||
//
|
||||
// if err := accessController.Authorized(r, access); err != nil {
|
||||
// if ctx, err := accessController.Authorized(ctx, access); err != nil {
|
||||
// if challenge, ok := err.(auth.Challenge) {
|
||||
// // Let the challenge write the response.
|
||||
// challenge.ServeHTTP(w, r)
|
||||
|
@ -31,10 +31,35 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Common errors used with this package.
|
||||
var (
|
||||
ErrNoRequestContext = errors.New("no http request in context")
|
||||
ErrNoAuthUserInfo = errors.New("no auth user info in context")
|
||||
)
|
||||
|
||||
// RequestFromContext returns the http request in the given context.
|
||||
// Returns ErrNoRequestContext if the context does not have an http
|
||||
// request associated with it.
|
||||
func RequestFromContext(ctx context.Context) (*http.Request, error) {
|
||||
if r, ok := ctx.Value("http.request").(*http.Request); r != nil && ok {
|
||||
return r, nil
|
||||
}
|
||||
return nil, ErrNoRequestContext
|
||||
}
|
||||
|
||||
// UserInfo carries information about
|
||||
// an autenticated/authorized client.
|
||||
type UserInfo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Resource describes a resource by type and name.
|
||||
type Resource struct {
|
||||
Type string
|
||||
|
@ -65,13 +90,16 @@ type Challenge interface {
|
|||
// and required access levels for a request. Implementations can support both
|
||||
// complete denial and http authorization challenges.
|
||||
type AccessController interface {
|
||||
// Authorized returns non-nil if the request is granted access. If one or
|
||||
// more Access structs are provided, the requested access will be compared
|
||||
// with what is available to the request. If the error is non-nil, access
|
||||
// should always be denied. The error may be of type Challenge, in which
|
||||
// case the caller may have the Challenge handle the request or choose
|
||||
// what action to take based on the Challenge header or response status.
|
||||
Authorized(req *http.Request, access ...Access) error
|
||||
// Authorized returns a non-nil error if the context is granted access and
|
||||
// returns a new authorized context. If one or more Access structs are
|
||||
// provided, the requested access will be compared with what is available
|
||||
// to the context. The given context will contain a "http.request" key with
|
||||
// a `*http.Request` value. If the error is non-nil, access should always
|
||||
// be denied. The error may be of type Challenge, in which case the caller
|
||||
// may have the Challenge handle the request or choose what action to take
|
||||
// based on the Challenge header or response status. The returned context
|
||||
// object should have a "auth.user" value set to a UserInfo struct.
|
||||
Authorized(ctx context.Context, access ...Access) (context.Context, error)
|
||||
}
|
||||
|
||||
// InitFunc is the type of an AccessController factory function and is used
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/docker/distribution/auth"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// accessController provides a simple implementation of auth.AccessController
|
||||
|
@ -41,7 +42,12 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
|
|||
|
||||
// Authorized simply checks for the existence of the authorization header,
|
||||
// responding with a bearer challenge if it doesn't exist.
|
||||
func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.Access) error {
|
||||
func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) {
|
||||
req, err := auth.RequestFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") == "" {
|
||||
challenge := challenge{
|
||||
realm: ac.realm,
|
||||
|
@ -56,10 +62,10 @@ func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.
|
|||
challenge.scope = strings.Join(scopes, " ")
|
||||
}
|
||||
|
||||
return &challenge
|
||||
return nil, &challenge
|
||||
}
|
||||
|
||||
return nil
|
||||
return context.WithValue(ctx, "auth.user", auth.UserInfo{Name: "silly"}), nil
|
||||
}
|
||||
|
||||
type challenge struct {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/docker/distribution/auth"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestSillyAccessController(t *testing.T) {
|
||||
|
@ -15,7 +16,9 @@ func TestSillyAccessController(t *testing.T) {
|
|||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := ac.Authorized(r); err != nil {
|
||||
ctx := context.WithValue(nil, "http.request", r)
|
||||
authCtx, err := ac.Authorized(ctx)
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case auth.Challenge:
|
||||
err.ServeHTTP(w, r)
|
||||
|
@ -25,6 +28,15 @@ func TestSillyAccessController(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
userInfo, ok := authCtx.Value("auth.user").(auth.UserInfo)
|
||||
if !ok {
|
||||
t.Fatal("silly accessController did not set auth.user context")
|
||||
}
|
||||
|
||||
if userInfo.Name != "silly" {
|
||||
t.Fatalf("expected user name %q, got %q", "silly", userInfo.Name)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
|
||||
"github.com/docker/distribution/auth"
|
||||
"github.com/docker/libtrust"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// accessSet maps a typed, named resource to
|
||||
|
@ -217,18 +217,23 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
|
|||
|
||||
// Authorized handles checking whether the given request is authorized
|
||||
// for actions on resources described by the given access items.
|
||||
func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Access) error {
|
||||
func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
|
||||
challenge := &authChallenge{
|
||||
realm: ac.realm,
|
||||
service: ac.service,
|
||||
accessSet: newAccessSet(accessItems...),
|
||||
}
|
||||
|
||||
req, err := auth.RequestFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts := strings.Split(req.Header.Get("Authorization"), " ")
|
||||
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
challenge.err = ErrTokenRequired
|
||||
return challenge
|
||||
return nil, challenge
|
||||
}
|
||||
|
||||
rawToken := parts[1]
|
||||
|
@ -236,7 +241,7 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
|
|||
token, err := NewToken(rawToken)
|
||||
if err != nil {
|
||||
challenge.err = err
|
||||
return challenge
|
||||
return nil, challenge
|
||||
}
|
||||
|
||||
verifyOpts := VerifyOptions{
|
||||
|
@ -248,18 +253,18 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
|
|||
|
||||
if err = token.Verify(verifyOpts); err != nil {
|
||||
challenge.err = err
|
||||
return challenge
|
||||
return nil, challenge
|
||||
}
|
||||
|
||||
accessSet := token.accessSet()
|
||||
for _, access := range accessItems {
|
||||
if !accessSet.contains(access) {
|
||||
challenge.err = ErrInsufficientScope
|
||||
return challenge
|
||||
return nil, challenge
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return context.WithValue(ctx, "auth.user", auth.UserInfo{Name: token.Claims.Subject}), nil
|
||||
}
|
||||
|
||||
// init handles registering the token auth backend.
|
||||
|
|
|
@ -47,7 +47,7 @@ type ClaimSet struct {
|
|||
JWTID string `json:"jti"`
|
||||
|
||||
// Private claims
|
||||
Access []*ResourceActions
|
||||
Access []*ResourceActions `json:"access"`
|
||||
}
|
||||
|
||||
// Header describes the header section of a JSON Web Token.
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
|
||||
"github.com/docker/distribution/auth"
|
||||
"github.com/docker/libtrust"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func makeRootKeys(numKeys int) ([]libtrust.PrivateKey, error) {
|
||||
|
@ -282,7 +283,8 @@ func TestAccessController(t *testing.T) {
|
|||
Action: "baz",
|
||||
}
|
||||
|
||||
err = accessController.Authorized(req, testAccess)
|
||||
ctx := context.WithValue(nil, "http.request", req)
|
||||
authCtx, err := accessController.Authorized(ctx, testAccess)
|
||||
challenge, ok := err.(auth.Challenge)
|
||||
if !ok {
|
||||
t.Fatal("accessController did not return a challenge")
|
||||
|
@ -292,6 +294,10 @@ func TestAccessController(t *testing.T) {
|
|||
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
|
||||
}
|
||||
|
||||
if authCtx != nil {
|
||||
t.Fatalf("expected nil auth context but got %s", authCtx)
|
||||
}
|
||||
|
||||
// 2. Supply an invalid token.
|
||||
token, err := makeTestToken(
|
||||
issuer, service,
|
||||
|
@ -308,7 +314,7 @@ func TestAccessController(t *testing.T) {
|
|||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
|
||||
|
||||
err = accessController.Authorized(req, testAccess)
|
||||
authCtx, err = accessController.Authorized(ctx, testAccess)
|
||||
challenge, ok = err.(auth.Challenge)
|
||||
if !ok {
|
||||
t.Fatal("accessController did not return a challenge")
|
||||
|
@ -318,6 +324,10 @@ func TestAccessController(t *testing.T) {
|
|||
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
|
||||
}
|
||||
|
||||
if authCtx != nil {
|
||||
t.Fatalf("expected nil auth context but got %s", authCtx)
|
||||
}
|
||||
|
||||
// 3. Supply a token with insufficient access.
|
||||
token, err = makeTestToken(
|
||||
issuer, service,
|
||||
|
@ -330,7 +340,7 @@ func TestAccessController(t *testing.T) {
|
|||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
|
||||
|
||||
err = accessController.Authorized(req, testAccess)
|
||||
authCtx, err = accessController.Authorized(ctx, testAccess)
|
||||
challenge, ok = err.(auth.Challenge)
|
||||
if !ok {
|
||||
t.Fatal("accessController did not return a challenge")
|
||||
|
@ -340,6 +350,10 @@ func TestAccessController(t *testing.T) {
|
|||
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope)
|
||||
}
|
||||
|
||||
if authCtx != nil {
|
||||
t.Fatalf("expected nil auth context but got %s", authCtx)
|
||||
}
|
||||
|
||||
// 4. Supply the token we need, or deserve, or whatever.
|
||||
token, err = makeTestToken(
|
||||
issuer, service,
|
||||
|
@ -348,7 +362,7 @@ func TestAccessController(t *testing.T) {
|
|||
Name: testAccess.Name,
|
||||
Actions: []string{testAccess.Action},
|
||||
}},
|
||||
rootKeys[0], 1, // Everything is valid except the key which signed it.
|
||||
rootKeys[0], 1,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -356,7 +370,17 @@ func TestAccessController(t *testing.T) {
|
|||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw()))
|
||||
|
||||
if err = accessController.Authorized(req, testAccess); err != nil {
|
||||
authCtx, err = accessController.Authorized(ctx, testAccess)
|
||||
if err != nil {
|
||||
t.Fatalf("accessController returned unexpected error: %s", err)
|
||||
}
|
||||
|
||||
userInfo, ok := authCtx.Value("auth.user").(auth.UserInfo)
|
||||
if !ok {
|
||||
t.Fatal("token accessController did not set auth.user context")
|
||||
}
|
||||
|
||||
if userInfo.Name != "foo" {
|
||||
t.Fatalf("expected user name %q, got %q", "foo", userInfo.Name)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue