0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

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

4
.gitignore vendored
View file

@ -16,3 +16,7 @@ bazel-*
coverage.txt coverage.txt
test/data/ test/data/
*.orig *.orig
.idea/
coverage.html
tags
vendor/

View file

@ -21,6 +21,10 @@ test:
$(shell mkdir -p test/data; cd test/data; ../scripts/gen_certs.sh; cd ${TOP_LEVEL}) $(shell mkdir -p test/data; cd test/data; ../scripts/gen_certs.sh; cd ${TOP_LEVEL})
go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic ./... go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic ./...
.PHONY: covhtml
covhtml:
go tool cover -html=coverage.txt -o coverage.html
.PHONY: check .PHONY: check
check: .bazel/golangcilint.yaml check: .bazel/golangcilint.yaml
golangci-lint --version || curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.21.0 golangci-lint --version || curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.21.0

View file

@ -134,6 +134,13 @@ go_repository(
version = "v1.1.0", version = "v1.1.0",
) )
go_repository(
name = "com_github_chartmuseum_auth",
importpath = "github.com/chartmuseum/auth",
sum = "h1:76rqyKtBdQAnC/YuT9ftL7OpLTDwfrfk8Ee8rD9OVOw=",
version = "v0.3.1",
)
go_repository( go_repository(
name = "com_github_client9_misspell", name = "com_github_client9_misspell",
importpath = "github.com/client9/misspell", importpath = "github.com/client9/misspell",
@ -1051,6 +1058,13 @@ go_repository(
version = "v0.0.0-20170223121919-b73f66626b33", version = "v0.0.0-20170223121919-b73f66626b33",
) )
go_repository(
name = "com_github_mitchellh_mapstructure",
importpath = "github.com/mitchellh/mapstructure",
sum = "h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=",
version = "v1.1.2",
)
go_repository( go_repository(
name = "com_github_nmcclain_asn1_ber", name = "com_github_nmcclain_asn1_ber",
importpath = "github.com/nmcclain/asn1-ber", importpath = "github.com/nmcclain/asn1-ber",
@ -1072,6 +1086,13 @@ go_repository(
version = "v0.0.0-20160315200505-970db520ece7", version = "v0.0.0-20160315200505-970db520ece7",
) )
go_repository(
name = "com_github_opencontainers_go_digest",
importpath = "github.com/opencontainers/go-digest",
sum = "h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=",
version = "v1.0.0-rc1",
)
go_repository( go_repository(
name = "com_github_phayes_freeport", name = "com_github_phayes_freeport",
importpath = "github.com/phayes/freeport", importpath = "github.com/phayes/freeport",

View file

@ -0,0 +1,20 @@
{
"version":"0.1.0-dev",
"storage":{
"rootDirectory":"/tmp/zot"
},
"http": {
"address":"127.0.0.1",
"port":"8080",
"auth": {
"bearer": {
"realm": "https://auth.myreg.io/auth/token",
"service": "myauth",
"cert": "/etc/zot/auth.crt"
}
}
},
"log":{
"level":"debug"
}
}

1
go.mod
View file

@ -4,6 +4,7 @@ go 1.13
require ( require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/chartmuseum/auth v0.3.1
github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a github.com/getlantern/deepcopy v0.0.0-20160317154340-7f45deb8130a
github.com/go-chi/chi v4.0.2+incompatible // indirect github.com/go-chi/chi v4.0.2+incompatible // indirect
github.com/go-ldap/ldap/v3 v3.1.3 github.com/go-ldap/ldap/v3 v3.1.3

10
go.sum
View file

@ -16,6 +16,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chartmuseum/auth v0.3.1 h1:76rqyKtBdQAnC/YuT9ftL7OpLTDwfrfk8Ee8rD9OVOw=
github.com/chartmuseum/auth v0.3.1/go.mod h1:hk7ENYpPKy5sEMkooBAuxBBtrsQjQtv9BNTLj7xZW2E=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@ -27,6 +29,7 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
@ -88,8 +91,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -139,7 +140,10 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -181,6 +185,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=

View file

@ -18,12 +18,15 @@ go_library(
"//errors:go_default_library", "//errors:go_default_library",
"//pkg/log:go_default_library", "//pkg/log:go_default_library",
"//pkg/storage:go_default_library", "//pkg/storage:go_default_library",
"@com_github_chartmuseum_auth//:go_default_library",
"@com_github_getlantern_deepcopy//:go_default_library", "@com_github_getlantern_deepcopy//:go_default_library",
"@com_github_go_ldap_ldap_v3//:go_default_library", "@com_github_go_ldap_ldap_v3//:go_default_library",
"@com_github_gorilla_handlers//:go_default_library", "@com_github_gorilla_handlers//:go_default_library",
"@com_github_gorilla_mux//:go_default_library", "@com_github_gorilla_mux//:go_default_library",
"@com_github_json_iterator_go//: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_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_opencontainers_image_spec//specs-go/v1:go_default_library",
"@com_github_swaggo_http_swagger//:go_default_library", "@com_github_swaggo_http_swagger//:go_default_library",
"@org_golang_x_crypto//bcrypt:go_default_library", "@org_golang_x_crypto//bcrypt:go_default_library",

View file

@ -13,19 +13,65 @@ import (
"time" "time"
"github.com/anuvu/zot/errors" "github.com/anuvu/zot/errors"
"github.com/chartmuseum/auth"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func authFail(w http.ResponseWriter, realm string, delay int) { const (
time.Sleep(time.Duration(delay) * time.Second) bearerAuthDefaultAccessEntryType = "repository"
w.Header().Set("WWW-Authenticate", realm) )
w.Header().Set("Content-Type", "application/json")
WriteJSON(w, http.StatusUnauthorized, NewError(UNAUTHORIZED)) 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 // 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 realm := c.Config.HTTP.Realm
if realm == "" { if realm == "" {
realm = "Authorization Required" realm = "Authorization Required"
@ -39,7 +85,7 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
if c.Config.HTTP.AllowReadAccess && if c.Config.HTTP.AllowReadAccess &&
c.Config.HTTP.TLS.CACert != "" && c.Config.HTTP.TLS.CACert != "" &&
r.TLS.VerifiedChains == nil && r.TLS.VerifiedChains == nil &&
r.Method != "GET" && r.Method != "HEAD" { r.Method != http.MethodGet && r.Method != http.MethodHead {
authFail(w, realm, 5) authFail(w, realm, 5)
return return
} }
@ -109,7 +155,7 @@ func BasicAuthHandler(c *Controller) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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 // Process request
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return 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))
}

View file

@ -28,6 +28,13 @@ type AuthConfig struct {
FailDelay int FailDelay int
HTPasswd AuthHTPasswd HTPasswd AuthHTPasswd
LDAP *LDAPConfig LDAP *LDAPConfig
Bearer *BearerConfig
}
type BearerConfig struct {
Realm string
Service string
Cert string
} }
type HTTPConfig struct { type HTTPConfig struct {

View file

@ -9,27 +9,51 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http"
"net/http/httptest"
"net/url"
"os" "os"
"regexp"
"strings"
"testing" "testing"
"time" "time"
"github.com/anuvu/zot/pkg/api" "github.com/anuvu/zot/pkg/api"
"github.com/chartmuseum/auth"
"github.com/mitchellh/mapstructure"
vldap "github.com/nmcclain/ldap" vldap "github.com/nmcclain/ldap"
godigest "github.com/opencontainers/go-digest"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1" "gopkg.in/resty.v1"
) )
const ( const (
BaseURL1 = "http://127.0.0.1:8081" BaseURL1 = "http://127.0.0.1:8081"
BaseURL2 = "http://127.0.0.1:8082" BaseURL2 = "http://127.0.0.1:8082"
BaseSecureURL2 = "https://127.0.0.1:8082" BaseURL3 = "http://127.0.0.1:8083"
SecurePort1 = "8081" BaseSecureURL2 = "https://127.0.0.1:8082"
SecurePort2 = "8082" SecurePort1 = "8081"
username = "test" SecurePort2 = "8082"
passphrase = "test" SecurePort3 = "8083"
ServerCert = "../../test/data/server.cert" username = "test"
ServerKey = "../../test/data/server.key" passphrase = "test"
CACert = "../../test/data/ca.crt" 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 { func makeHtpasswdFile() string {
@ -782,3 +806,213 @@ func TestBasicAuthWithLDAP(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, 200) 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
}

View file

@ -51,7 +51,7 @@ func NewRouteHandler(c *Controller) *RouteHandler {
} }
func (rh *RouteHandler) SetupRoutes() { 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 := rh.c.Router.PathPrefix(RoutePrefix).Subrouter()
{ {
g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()), g.HandleFunc(fmt.Sprintf("/{name:%s}/tags/list", NameRegexp.String()),