reg/auth: remove contexts from Authorized method

The details of how request-scoped information is propagated through the
registry server app should be left as private implementation details so
they can be changed without fear of breaking compatibility with
third-party code which imports the distribution module. The
AccessController interface unnecessarily bakes into the public API
details of how authorization grants are propagated through request
contexts. In practice the only values the in-tree authorizers attach to
the request contexts are the UserInfo and Resources for the request.
Change the AccessController interface to return the UserInfo and
Resources directly to allow us to change how request contexts are used
within the app without altering the AccessController interface contract.

Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
Cory Snider 2023-10-24 16:41:54 -04:00
parent 49e22cbf3e
commit bd80d7590d
8 changed files with 53 additions and 53 deletions

View file

@ -76,6 +76,12 @@ type Access struct {
Action string Action string
} }
// Grant describes the permitted level of access for an authorized request.
type Grant struct {
User UserInfo // The authenticated user for the request.
Resources []Resource // The list of resources which have been authorized for the request.
}
// Challenge is a special error type which is used for HTTP 401 Unauthorized // Challenge is a special error type which is used for HTTP 401 Unauthorized
// responses and is able to write the response with WWW-Authenticate challenge // responses and is able to write the response with WWW-Authenticate challenge
// header values based on the error. // header values based on the error.
@ -93,15 +99,15 @@ type Challenge interface {
// and required access levels for a request. Implementations can support both // and required access levels for a request. Implementations can support both
// complete denial and http authorization challenges. // complete denial and http authorization challenges.
type AccessController interface { type AccessController interface {
// Authorized returns a nil error if the request is granted access and // Authorized determines if the request is granted access. If one or more
// returns a new authorized context. If one or more Access structs are // Access structs are provided, the requested access will be compared with
// provided, the requested access will be compared with what is available // what is available to the request.
// to the request. Access is denied if the error is non-nil. The error may //
// be of type Challenge, in which case the caller may have the Challenge // Return a Grant to grant the request access. Return an error to deny
// handle the request or choose what action to take based on the Challenge // access. The error may be of type Challenge, in which case the caller may
// header or response status. The returned context object should be derived // have the Challenge handle the request or choose what action to take based
// from r.Context() and have a "auth.user" value set to a UserInfo struct. // on the Challenge header or response status.
Authorized(r *http.Request, access ...Access) (context.Context, error) Authorized(r *http.Request, access ...Access) (*Grant, error)
} }
// CredentialAuthenticator is an object which is able to authenticate credentials // CredentialAuthenticator is an object which is able to authenticate credentials

View file

@ -49,7 +49,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
return &accessController{realm: realm.(string), path: path}, nil return &accessController{realm: realm.(string), path: path}, nil
} }
func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.Access) (context.Context, error) { func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.Access) (*auth.Grant, error) {
username, password, ok := req.BasicAuth() username, password, ok := req.BasicAuth()
if !ok { if !ok {
return nil, &challenge{ return nil, &challenge{
@ -94,7 +94,7 @@ func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.
} }
} }
return auth.WithUser(req.Context(), auth.UserInfo{Name: username}), nil return &auth.Grant{User: auth.UserInfo{Name: username}}, nil
} }
// challenge implements the auth.Challenge interface. // challenge implements the auth.Challenge interface.

View file

@ -43,7 +43,7 @@ func TestBasicAccessController(t *testing.T) {
userNumber := 0 userNumber := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authCtx, err := accessController.Authorized(r) grant, err := accessController.Authorized(r)
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case auth.Challenge: case auth.Challenge:
@ -55,13 +55,12 @@ func TestBasicAccessController(t *testing.T) {
} }
} }
userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if grant == nil {
if !ok { t.Fatal("basic accessController did not return auth grant")
t.Fatal("basic accessController did not set auth.user context")
} }
if userInfo.Name != testUsers[userNumber] { if grant.User.Name != testUsers[userNumber] {
t.Fatalf("expected user name %q, got %q", testUsers[userNumber], userInfo.Name) t.Fatalf("expected user name %q, got %q", testUsers[userNumber], grant.User.Name)
} }
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View file

@ -8,12 +8,10 @@
package silly package silly
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"github.com/distribution/distribution/v3/internal/dcontext"
"github.com/distribution/distribution/v3/registry/auth" "github.com/distribution/distribution/v3/registry/auth"
) )
@ -43,7 +41,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
// Authorized simply checks for the existence of the authorization header, // Authorized simply checks for the existence of the authorization header,
// responding with a bearer challenge if it doesn't exist. // responding with a bearer challenge if it doesn't exist.
func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.Access) (context.Context, error) { func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.Access) (*auth.Grant, error) {
if req.Header.Get("Authorization") == "" { if req.Header.Get("Authorization") == "" {
challenge := challenge{ challenge := challenge{
realm: ac.realm, realm: ac.realm,
@ -61,10 +59,7 @@ func (ac *accessController) Authorized(req *http.Request, accessRecords ...auth.
return nil, &challenge return nil, &challenge
} }
ctx := auth.WithUser(req.Context(), auth.UserInfo{Name: "silly"}) return &auth.Grant{User: auth.UserInfo{Name: "silly"}}, nil
ctx = dcontext.WithLogger(ctx, dcontext.GetLogger(ctx, auth.UserNameKey, auth.UserKey))
return ctx, nil
} }
type challenge struct { type challenge struct {

View file

@ -15,7 +15,7 @@ func TestSillyAccessController(t *testing.T) {
} }
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authCtx, err := ac.Authorized(r) grant, err := ac.Authorized(r)
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case auth.Challenge: case auth.Challenge:
@ -27,13 +27,12 @@ func TestSillyAccessController(t *testing.T) {
} }
} }
userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if grant == nil {
if !ok { t.Fatal("silly accessController did not return auth grant")
t.Fatal("silly accessController did not set auth.user context")
} }
if userInfo.Name != "silly" { if grant.User.Name != "silly" {
t.Fatalf("expected user name %q, got %q", "silly", userInfo.Name) t.Fatalf("expected user name %q, got %q", "silly", grant.User.Name)
} }
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)

View file

@ -1,7 +1,6 @@
package token package token
import ( import (
"context"
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
@ -291,7 +290,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
// Authorized handles checking whether the given request is authorized // Authorized handles checking whether the given request is authorized
// for actions on resources described by the given access items. // for actions on resources described by the given access items.
func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Access) (context.Context, error) { func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Access) (*auth.Grant, error) {
challenge := &authChallenge{ challenge := &authChallenge{
realm: ac.realm, realm: ac.realm,
autoRedirect: ac.autoRedirect, autoRedirect: ac.autoRedirect,
@ -332,9 +331,10 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
} }
} }
ctx := auth.WithResources(req.Context(), claims.resources()) return &auth.Grant{
User: auth.UserInfo{Name: claims.Subject},
return auth.WithUser(ctx, auth.UserInfo{Name: claims.Subject}), nil Resources: claims.resources(),
}, nil
} }
// init handles registering the token auth backend. // init handles registering the token auth backend.

View file

@ -465,7 +465,7 @@ func TestAccessController(t *testing.T) {
Action: "baz", Action: "baz",
} }
authCtx, err := accessController.Authorized(req, testAccess) grant, err := accessController.Authorized(req, testAccess)
challenge, ok := err.(auth.Challenge) challenge, ok := err.(auth.Challenge)
if !ok { if !ok {
t.Fatal("accessController did not return a challenge") t.Fatal("accessController did not return a challenge")
@ -475,8 +475,8 @@ func TestAccessController(t *testing.T) {
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired) t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
} }
if authCtx != nil { if grant != nil {
t.Fatalf("expected nil auth context but got %s", authCtx) t.Fatalf("expected nil auth grant but got %#v", grant)
} }
// 2. Supply an invalid token. // 2. Supply an invalid token.
@ -500,7 +500,7 @@ func TestAccessController(t *testing.T) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Raw)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Raw))
authCtx, err = accessController.Authorized(req, testAccess) grant, err = accessController.Authorized(req, testAccess)
challenge, ok = err.(auth.Challenge) challenge, ok = err.(auth.Challenge)
if !ok { if !ok {
t.Fatal("accessController did not return a challenge") t.Fatal("accessController did not return a challenge")
@ -510,8 +510,8 @@ func TestAccessController(t *testing.T) {
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired) t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired)
} }
if authCtx != nil { if grant != nil {
t.Fatalf("expected nil auth context but got %s", authCtx) t.Fatalf("expected nil auth grant but got %#v", grant)
} }
// create a valid jwk // create a valid jwk
@ -532,7 +532,7 @@ func TestAccessController(t *testing.T) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Raw)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Raw))
authCtx, err = accessController.Authorized(req, testAccess) grant, err = accessController.Authorized(req, testAccess)
challenge, ok = err.(auth.Challenge) challenge, ok = err.(auth.Challenge)
if !ok { if !ok {
t.Fatal("accessController did not return a challenge") t.Fatal("accessController did not return a challenge")
@ -542,8 +542,8 @@ func TestAccessController(t *testing.T) {
t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope) t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope)
} }
if authCtx != nil { if grant != nil {
t.Fatalf("expected nil auth context but got %s", authCtx) t.Fatalf("expected nil auth grant but got %#v", grant)
} }
// 4. Supply the token we need, or deserve, or whatever. // 4. Supply the token we need, or deserve, or whatever.
@ -562,18 +562,13 @@ func TestAccessController(t *testing.T) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Raw)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.Raw))
authCtx, err = accessController.Authorized(req, testAccess) grant, err = accessController.Authorized(req, testAccess)
if err != nil { if err != nil {
t.Fatalf("accessController returned unexpected error: %s", err) t.Fatalf("accessController returned unexpected error: %s", err)
} }
userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if grant.User.Name != "foo" {
if !ok { t.Fatalf("expected user name %q, got %q", "foo", grant.User.Name)
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)
} }
// 5. Supply a token with full admin rights, which is represented as "*". // 5. Supply a token with full admin rights, which is represented as "*".

View file

@ -797,7 +797,7 @@ func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Cont
accessRecords = appendCatalogAccessRecord(accessRecords, r) accessRecords = appendCatalogAccessRecord(accessRecords, r)
} }
ctx, err := app.accessController.Authorized(r.WithContext(context.Context), accessRecords...) grant, err := app.accessController.Authorized(r.WithContext(context.Context), accessRecords...)
if err != nil { if err != nil {
switch err := err.(type) { switch err := err.(type) {
case auth.Challenge: case auth.Challenge:
@ -818,6 +818,12 @@ func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Cont
return err return err
} }
if grant == nil {
return fmt.Errorf("access controller returned neither an access grant nor an error")
}
ctx := auth.WithUser(context.Context, grant.User)
ctx = auth.WithResources(ctx, grant.Resources)
dcontext.GetLogger(ctx, auth.UserNameKey).Info("authorized request") dcontext.GetLogger(ctx, auth.UserNameKey).Info("authorized request")
// TODO(stevvooe): This pattern needs to be cleaned up a bit. One context // TODO(stevvooe): This pattern needs to be cleaned up a bit. One context