Skip to content

Commit 7940966

Browse files
author
Michal Minar
committed
Handle cross repository mount
During a push with a cross-repo mount, authorize only against target repository, not against source repository. In OpenShift we allow to pull any blob stored in the registry as long as the user is authorized to pull from repository he requested. Signed-off-by: Michal Minar <[email protected]>
1 parent 1b04cb2 commit 7940966

File tree

3 files changed

+161
-11
lines changed

3 files changed

+161
-11
lines changed

pkg/dockerregistry/server/auth.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"net/url"
89
"strings"
910

1011
log "github.com/Sirupsen/logrus"
@@ -196,7 +197,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
196197
}
197198
verifiedPrune = true
198199
default:
199-
if err := verifyImageStreamAccess(ctx, imageStreamNS, imageStreamName, verb, osClient); err != nil {
200+
if err := authorizeRepositoryAccess(ctx, imageStreamNS, imageStreamName, verb, osClient); err != nil {
200201
return nil, ac.wrapErr(err)
201202
}
202203
}
@@ -272,6 +273,59 @@ func verifyOpenShiftUser(ctx context.Context, client client.UsersInterface) erro
272273
return nil
273274
}
274275

276+
// authorizeRepositoryAccess returns nil if the given client is granted access to the given
277+
// <namespace>/<imageRepo> repository. Otherwise the access is denied.
278+
func authorizeRepositoryAccess(ctx context.Context, namespace, imageRepo, verb string, client client.LocalSubjectAccessReviewsNamespacer) error {
279+
if isCrossRepoMountFromSourceRepository(ctx, namespace, imageRepo, verb) {
280+
return nil
281+
}
282+
283+
return verifyImageStreamAccess(ctx, namespace, imageRepo, verb, client)
284+
}
285+
286+
// isCrossRepoMountFromSourceRepository handles a case of accessing source repository requested by a cross
287+
// repository mount during a blob upload. During a push with the cross-repo mount, there are two authorization
288+
// checks:
289+
//
290+
// 1. repository:<source-namespace>/<source-name>:pull
291+
// 2. repository:<target-namespace>/<target-name>:push
292+
//
293+
// This function always grants access in the first case because OpenShift registry treats all the local blobs
294+
// as accessible once authorized to access given repository (target one).
295+
func isCrossRepoMountFromSourceRepository(ctx context.Context, namespace, imageRepo, verb string) bool {
296+
if verb != "get" {
297+
return false
298+
}
299+
if method := strings.ToLower(getStringValueFromContext(ctx, "http.request.method", "")); method != "post" {
300+
return false
301+
}
302+
303+
uriString := getStringValue(ctx, "http.request.uri")
304+
uri, err := url.Parse(uriString)
305+
if err != nil {
306+
context.GetLogger(ctx).Warnf("failed to parse requested uri %q: %v", uri, err)
307+
return false
308+
}
309+
from, exists := uri.Query()["from"]
310+
if !exists {
311+
context.GetLogger(ctx).Infof("cross-repo mount check: missing from attribute in query")
312+
return false
313+
}
314+
repoName := fmt.Sprintf("%s/%s", namespace, imageRepo)
315+
for _, f := range from {
316+
if f == repoName {
317+
context.GetLogger(ctx).Infof("authorized a cross-repo mount from=%q to repository %q",
318+
repoName, getStringValueFromContext(ctx, "vars.name", "UNKNOWN"))
319+
return true
320+
}
321+
}
322+
323+
return false
324+
}
325+
326+
// verifyImageStreamAccess returns nil if the given client is granted access to an image stream identified by
327+
// <namespace>/<imageRepo>. Otherwise the access is denied. The user embedded in given client must be able to
328+
// <verb> (get/update) imagestreams/layers resource in the <namespace> to have the access granted.
275329
func verifyImageStreamAccess(ctx context.Context, namespace, imageRepo, verb string, client client.LocalSubjectAccessReviewsNamespacer) error {
276330
sar := authorizationapi.LocalSubjectAccessReview{
277331
Action: authorizationapi.AuthorizationAttributes{
@@ -299,6 +353,9 @@ func verifyImageStreamAccess(ctx context.Context, namespace, imageRepo, verb str
299353
return nil
300354
}
301355

356+
// verifyPruneAccess returns nil if the given client is granted access to images resource. Otherwise the
357+
// access is denied. The user embedded in given client must be able to delete images resource in a cluster to
358+
// have the access granted.
302359
func verifyPruneAccess(ctx context.Context, client client.SubjectAccessReviews) error {
303360
sar := authorizationapi.SubjectAccessReview{
304361
Action: authorizationapi.AuthorizationAttributes{
@@ -321,3 +378,13 @@ func verifyPruneAccess(ctx context.Context, client client.SubjectAccessReviews)
321378
}
322379
return nil
323380
}
381+
382+
// getStringValueFromContext returns a value stored in given context under given key if it exists. The
383+
// defaultValue will be returned otherwise.
384+
func getStringValueFromContext(ctx context.Context, key, defaultValue string) string {
385+
value := ctx.Value(key)
386+
if str, ok := value.(string); ok {
387+
return str
388+
}
389+
return defaultValue
390+
}

pkg/dockerregistry/server/auth_test.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ func TestAccessController(t *testing.T) {
9191

9292
tests := map[string]struct {
9393
access []auth.Access
94+
uri string
95+
method string
9496
basicToken string
9597
openshiftResponses []response
9698
expectedError error
@@ -263,6 +265,92 @@ func TestAccessController(t *testing.T) {
263265
"POST /oapi/v1/subjectaccessreviews",
264266
},
265267
},
268+
"cross-repo mount": {
269+
access: []auth.Access{
270+
{
271+
Resource: auth.Resource{
272+
Type: "repository",
273+
Name: "crossrepo/source",
274+
},
275+
Action: "pull",
276+
},
277+
{
278+
Resource: auth.Resource{
279+
Type: "repository",
280+
Name: "foo/destination",
281+
},
282+
Action: "push",
283+
},
284+
},
285+
uri: "/v2/crossrepo/destination/blobs/uploads/?from=crossrepo/source&mount=sha256:da71393503ec9136cf62056c233f5d25b878e372c840170d91d65f8cdf94def2",
286+
method: "POST",
287+
basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=",
288+
openshiftResponses: []response{
289+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "crossrepo", Allowed: true, Reason: "authorized!"})},
290+
},
291+
expectedError: nil,
292+
expectedChallenge: false,
293+
expectedActions: []string{"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews"},
294+
},
295+
"cross-repo mount missing from attribute": {
296+
access: []auth.Access{
297+
{
298+
Resource: auth.Resource{
299+
Type: "repository",
300+
Name: "crossrepo/source",
301+
},
302+
Action: "pull",
303+
},
304+
{
305+
Resource: auth.Resource{
306+
Type: "repository",
307+
Name: "foo/destination",
308+
},
309+
Action: "push",
310+
},
311+
},
312+
uri: "/v2/foo/destination/blobs/uploads/?mount=sha256:da71393503ec9136cf62056c233f5d25b878e372c840170d91d65f8cdf94def2",
313+
method: "POST",
314+
basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=",
315+
openshiftResponses: []response{
316+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "crossrepo", Allowed: false, Reason: "no!"})},
317+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: true, Reason: "authorized!"})},
318+
},
319+
expectedError: ErrOpenShiftAccessDenied,
320+
expectedChallenge: true,
321+
expectedActions: []string{"POST /oapi/v1/namespaces/crossrepo/localsubjectaccessreviews"},
322+
},
323+
"cross-repo mount with unexpected method": {
324+
access: []auth.Access{
325+
{
326+
Resource: auth.Resource{
327+
Type: "repository",
328+
Name: "crossrepo/source",
329+
},
330+
Action: "pull",
331+
},
332+
{
333+
Resource: auth.Resource{
334+
Type: "repository",
335+
Name: "foo/destination",
336+
},
337+
Action: "push",
338+
},
339+
},
340+
uri: "/v2/crossrepo/destination/blobs/uploads/?from=crossrepo/source&mount=sha256:da71393503ec9136cf62056c233f5d25b878e372c840170d91d65f8cdf94def2",
341+
method: "PUT",
342+
basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=",
343+
openshiftResponses: []response{
344+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "crossrepo", Allowed: true, Reason: "authorized!"})},
345+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "foo", Allowed: false, Reason: "authorized!"})},
346+
},
347+
expectedError: ErrOpenShiftAccessDenied,
348+
expectedChallenge: true,
349+
expectedActions: []string{
350+
"POST /oapi/v1/namespaces/crossrepo/localsubjectaccessreviews",
351+
"POST /oapi/v1/namespaces/foo/localsubjectaccessreviews",
352+
},
353+
},
266354
}
267355

268356
for k, test := range tests {
@@ -274,7 +362,11 @@ func TestAccessController(t *testing.T) {
274362
if len(test.basicToken) > 0 {
275363
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", test.basicToken))
276364
}
277-
ctx := context.WithValue(context.Background(), "http.request", req)
365+
ctx := context.WithValues(context.Background(), map[string]interface{}{
366+
"http.request": req,
367+
"http.request.uri": test.uri,
368+
"http.request.method": test.method,
369+
})
278370

279371
server, actions := simulateOpenShiftMaster(test.openshiftResponses)
280372
DefaultRegistryClient = NewRegistryClient(&clientcmd.Config{

test/end-to-end/core.sh

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,15 +220,6 @@ echo "[INFO] Run pod diagnostics"
220220
# Requires a node to run the origin-deployer pod; expects registry deployed, deployer image pulled
221221
os::cmd::expect_success_and_text 'oadm diagnostics DiagnosticPod --images='"'""${USE_IMAGES}""'" 'Running diagnostic: PodCheckDns'
222222

223-
# This needed because docker 1.10+ utilizes cross-repo mount feature. Docker client keeps track of source
224-
# repositories for all the blobs. If it uploads a blob to a repository of registry having an entry in this
225-
# mapping, it will request a mount from this repository. The authorization check is done for both destination
226-
# and mounted repository. Without the next statement, the auth check for mounted repository
227-
# (cache/ruby-22-centos7) will fail.
228-
# TODO: remove this once the registry can access global blob store without a layer link authorization check
229-
oc policy -n cache add-role-to-user system:image-puller system:serviceaccount:test:builder
230-
231-
232223
echo "[INFO] Applying STI application config"
233224
os::cmd::expect_success "oc create -f ${STI_CONFIG_FILE}"
234225

0 commit comments

Comments
 (0)