Add support for bearer/token auth

New options added to configuration file to reference a public key used
to validate authorization tokens signed by an auth server with
corresponding private key.

Resolves #24

Signed-off-by: Peter Engelbert <pmengelbert@gmail.com>
This commit is contained in:
Peter Engelbert
2020-01-24 15:32:38 -06:00
parent 10d8a8650f
commit 268b4088fd
11 changed files with 374 additions and 21 deletions
+3
View File
@@ -18,12 +18,15 @@ go_library(
"//errors:go_default_library",
"//pkg/log:go_default_library",
"//pkg/storage:go_default_library",
"@com_github_chartmuseum_auth//:go_default_library",
"@com_github_getlantern_deepcopy//:go_default_library",
"@com_github_go_ldap_ldap_v3//:go_default_library",
"@com_github_gorilla_handlers//:go_default_library",
"@com_github_gorilla_mux//:go_default_library",
"@com_github_json_iterator_go//:go_default_library",
"@com_github_mitchellh_mapstructure//:go_default_library",
"@com_github_opencontainers_distribution_spec//:go_default_library",
"@com_github_opencontainers_go_digest//:go_default_library",
"@com_github_opencontainers_image_spec//specs-go/v1:go_default_library",
"@com_github_swaggo_http_swagger//:go_default_library",
"@org_golang_x_crypto//bcrypt:go_default_library",
+61 -8
View File
@@ -13,19 +13,65 @@ import (
"time"
"github.com/anuvu/zot/errors"
"github.com/chartmuseum/auth"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
)
func authFail(w http.ResponseWriter, realm string, delay int) {
time.Sleep(time.Duration(delay) * time.Second)
w.Header().Set("WWW-Authenticate", realm)
w.Header().Set("Content-Type", "application/json")
WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED))
const (
bearerAuthDefaultAccessEntryType = "repository"
)
func AuthHandler(c *Controller) mux.MiddlewareFunc {
if c.Config.HTTP.Auth != nil &&
c.Config.HTTP.Auth.Bearer != nil &&
c.Config.HTTP.Auth.Bearer.Cert != "" &&
c.Config.HTTP.Auth.Bearer.Realm != "" &&
c.Config.HTTP.Auth.Bearer.Service != "" {
return bearerAuthHandler(c)
}
return basicAuthHandler(c)
}
func bearerAuthHandler(c *Controller) mux.MiddlewareFunc {
authorizer, err := auth.NewAuthorizer(&auth.AuthorizerOptions{
Realm: c.Config.HTTP.Auth.Bearer.Realm,
Service: c.Config.HTTP.Auth.Bearer.Service,
PublicKeyPath: c.Config.HTTP.Auth.Bearer.Cert,
AccessEntryType: bearerAuthDefaultAccessEntryType,
})
if err != nil {
c.Log.Panic().Err(err).Msg("error creating bearer authorizer")
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
header := r.Header.Get("Authorization")
action := auth.PullAction
if m := r.Method; m != http.MethodGet && m != http.MethodHead {
action = auth.PushAction
}
permissions, err := authorizer.Authorize(header, action, name)
if err != nil {
c.Log.Error().Err(err).Msg("issue parsing Authorization header")
w.Header().Set("Content-Type", "application/json")
WriteJSON(w, http.StatusInternalServerError, NewError(UNSUPPORTED))
return
}
if !permissions.Allowed {
authFail(w, permissions.WWWAuthenticateHeader, 0)
return
}
next.ServeHTTP(w, r)
})
}
}
// nolint (gocyclo) - we use closure making this a complex subroutine
func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
func basicAuthHandler(c *Controller) mux.MiddlewareFunc {
realm := c.Config.HTTP.Realm
if realm == "" {
realm = "Authorization Required"
@@ -39,7 +85,7 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
if c.Config.HTTP.AllowReadAccess &&
c.Config.HTTP.TLS.CACert != "" &&
r.TLS.VerifiedChains == nil &&
r.Method != "GET" && r.Method != "HEAD" {
r.Method != http.MethodGet && r.Method != http.MethodHead {
authFail(w, realm, 5)
return
}
@@ -109,7 +155,7 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if (r.Method == "GET" || r.Method == "HEAD") && c.Config.HTTP.AllowReadAccess {
if (r.Method == http.MethodGet || r.Method == http.MethodHead) && c.Config.HTTP.AllowReadAccess {
// Process request
next.ServeHTTP(w, r)
return
@@ -167,3 +213,10 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
})
}
}
func authFail(w http.ResponseWriter, realm string, delay int) {
time.Sleep(time.Duration(delay) * time.Second)
w.Header().Set("WWW-Authenticate", realm)
w.Header().Set("Content-Type", "application/json")
WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED))
}
+7
View File
@@ -28,6 +28,13 @@ type AuthConfig struct {
FailDelay int
HTPasswd AuthHTPasswd
LDAP *LDAPConfig
Bearer *BearerConfig
}
type BearerConfig struct {
Realm string
Service string
Cert string
}
type HTTPConfig struct {
+244 -10
View File
@@ -9,27 +9,51 @@ import (
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"regexp"
"strings"
"testing"
"time"
"github.com/anuvu/zot/pkg/api"
"github.com/chartmuseum/auth"
"github.com/mitchellh/mapstructure"
vldap "github.com/nmcclain/ldap"
godigest "github.com/opencontainers/go-digest"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
)
const (
BaseURL1 = "http://127.0.0.1:8081"
BaseURL2 = "http://127.0.0.1:8082"
BaseSecureURL2 = "https://127.0.0.1:8082"
SecurePort1 = "8081"
SecurePort2 = "8082"
username = "test"
passphrase = "test"
ServerCert = "../../test/data/server.cert"
ServerKey = "../../test/data/server.key"
CACert = "../../test/data/ca.crt"
BaseURL1 = "http://127.0.0.1:8081"
BaseURL2 = "http://127.0.0.1:8082"
BaseURL3 = "http://127.0.0.1:8083"
BaseSecureURL2 = "https://127.0.0.1:8082"
SecurePort1 = "8081"
SecurePort2 = "8082"
SecurePort3 = "8083"
username = "test"
passphrase = "test"
ServerCert = "../../test/data/server.cert"
ServerKey = "../../test/data/server.key"
CACert = "../../test/data/ca.crt"
AuthorizedNamespace = "everyone/isallowed"
UnauthorizedNamespace = "fortknox/notallowed"
)
type (
accessTokenResponse struct {
AccessToken string `json:"access_token"`
}
authHeader struct {
Realm string
Service string
Scope string
}
)
func makeHtpasswdFile() string {
@@ -782,3 +806,213 @@ func TestBasicAuthWithLDAP(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200)
})
}
func TestBearerAuth(t *testing.T) {
Convey("Make a new controller", t, func() {
authTestServer := makeAuthTestServer()
defer authTestServer.Close()
config := api.NewConfig()
config.HTTP.Port = SecurePort3
u, err := url.Parse(authTestServer.URL)
So(err, ShouldBeNil)
config.HTTP.Auth = &api.AuthConfig{
Bearer: &api.BearerConfig{
Cert: ServerCert,
Realm: authTestServer.URL + "/auth/token",
Service: u.Host,
},
}
c := api.NewController(config)
dir, err := ioutil.TempDir("", "oci-repo-test")
So(err, ShouldBeNil)
defer os.RemoveAll(dir)
c.Config.Storage.RootDirectory = dir
go func() {
// this blocks
if err := c.Run(); err != nil {
return
}
}()
// wait till ready
for {
_, err := resty.R().Get(BaseURL3)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
defer func() {
ctx := context.Background()
_ = c.Server.Shutdown(ctx)
}()
blob := []byte("hello, blob!")
digest := godigest.FromBytes(blob).String()
resp, err := resty.R().Post(BaseURL3 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
authorizationHeader := parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
var goodToken accessTokenResponse
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
Post(BaseURL3 + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 202)
loc := resp.Header().Get("Location")
resp, err = resty.R().
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
SetHeader("Content-Type", "application/octet-stream").
SetQueryParam("digest", digest).
SetBody(blob).
Put(BaseURL3 + loc)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
SetHeader("Content-Type", "application/octet-stream").
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
SetQueryParam("digest", digest).
SetBody(blob).
Put(BaseURL3 + loc)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 201)
resp, err = resty.R().
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
Get(BaseURL3 + "/v2/" + AuthorizedNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
Get(BaseURL3 + "/v2/" + AuthorizedNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
resp, err = resty.R().
Post(BaseURL3 + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
authorizationHeader = parseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
SetQueryParam("scope", authorizationHeader.Scope).
Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
var badToken accessTokenResponse
err = json.Unmarshal(resp.Body(), &badToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
Post(BaseURL3 + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 401)
})
}
func makeAuthTestServer() *httptest.Server {
cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{
PrivateKeyPath: ServerKey,
Audience: "Zot Registry",
Issuer: "Zot",
AddKIDHeader: true,
})
if err != nil {
panic(err)
}
authTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
scope := r.URL.Query().Get("scope")
parts := strings.Split(scope, ":")
name := parts[1]
actions := strings.Split(parts[2], ",")
if name == UnauthorizedNamespace {
actions = []string{}
}
access := []auth.AccessEntry{
{
Name: name,
Type: "repository",
Actions: actions,
},
}
token, err := cmTokenGenerator.GenerateToken(access, time.Minute*1)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"access_token": "%s"}`, token)
}))
return authTestServer
}
func parseBearerAuthHeader(authHeaderRaw string) *authHeader {
re := regexp.MustCompile(`([a-zA-z]+)="(.+?)"`)
matches := re.FindAllStringSubmatch(authHeaderRaw, -1)
m := make(map[string]string)
for i := 0; i < len(matches); i++ {
m[matches[i][1]] = matches[i][2]
}
var h authHeader
if err := mapstructure.Decode(m, &h); err != nil {
panic(err)
}
return &h
}
+1 -1
View File
@@ -51,7 +51,7 @@ func NewRouteHandler(c *Controller) *RouteHandler {
}
func (rh *RouteHandler) SetupRoutes() {
rh.c.Router.Use(BasicAuthHandler(rh.c))
rh.c.Router.Use(AuthHandler(rh.c))
g := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
{
g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),