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:
parent
10d8a8650f
commit
268b4088fd
11 changed files with 374 additions and 21 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -16,3 +16,7 @@ bazel-*
|
||||||
coverage.txt
|
coverage.txt
|
||||||
test/data/
|
test/data/
|
||||||
*.orig
|
*.orig
|
||||||
|
.idea/
|
||||||
|
coverage.html
|
||||||
|
tags
|
||||||
|
vendor/
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -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
|
||||||
|
|
21
WORKSPACE
21
WORKSPACE
|
@ -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",
|
||||||
|
|
20
examples/config-bearer-auth.json
Normal file
20
examples/config-bearer-auth.json
Normal 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
1
go.mod
|
@ -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
10
go.sum
|
@ -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=
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
Loading…
Reference in a new issue