Skip to content

Commit fb49049

Browse files
author
Michal Minar
committed
POC for deferred auth errors
1 parent eb9ef75 commit fb49049

File tree

7 files changed

+392
-30
lines changed

7 files changed

+392
-30
lines changed

pkg/dockerregistry/server/auth.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,24 @@ import (
2121
imageapi "github.com/openshift/origin/pkg/image/api"
2222
)
2323

24+
// crossRepoMountAuthError
25+
type crossRepoMountAuthError struct {
26+
targetRepoName string
27+
sourceRepoName string
28+
err error
29+
}
30+
31+
func (e crossRepoMountAuthError) Error() string {
32+
if e.targetRepoName != "" {
33+
return fmt.Sprintf("unauthorized to read from source repository %q during cross-repo mount to %q: %v",
34+
e.sourceRepoName, e.targetRepoName, e.err)
35+
}
36+
return fmt.Sprintf("unauthorized to read from source repository %q during cross-repo mount: %v",
37+
e.sourceRepoName, e.targetRepoName, e.err)
38+
}
39+
40+
type deferredErrors map[string]error
41+
2442
// DefaultRegistryClient is exposed for testing the registry with fake client.
2543
var DefaultRegistryClient = NewRegistryClient(clientcmd.NewConfig().BindToFile())
2644

@@ -61,6 +79,27 @@ func UserClientFrom(ctx context.Context) (client.Interface, bool) {
6179
return userClient, ok
6280
}
6381

82+
const authPerformedKey = "openshift.auth.performed"
83+
84+
func WithAuthPerformed(parent context.Context) context.Context {
85+
return context.WithValue(parent, authPerformedKey, true)
86+
}
87+
88+
func AuthPerformed(ctx context.Context) bool {
89+
authPerformed, ok := ctx.Value(authPerformedKey).(bool)
90+
return ok && authPerformed
91+
}
92+
93+
const deferredErrorsKey = "openshift.auth.deferredErrors"
94+
95+
func WithDeferredErrors(parent context.Context, errs deferredErrors) context.Context {
96+
return context.WithValue(parent, deferredErrorsKey, errs)
97+
}
98+
func DeferredErrorsFrom(ctx context.Context) (deferredErrors, bool) {
99+
errs, ok := ctx.Value(deferredErrorsKey).(deferredErrors)
100+
return errs, ok
101+
}
102+
64103
type AccessController struct {
65104
realm string
66105
config restclient.Config
@@ -160,6 +199,11 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
160199
}
161200
}
162201

202+
// pushChecks remembers which ns/name pairs had push access checks done
203+
pushChecks := map[string]struct{}{}
204+
// possibleCrossMountErrors holds errors which may be related to cross mount errors
205+
possibleCrossMountErrors := deferredErrors{}
206+
163207
verifiedPrune := false
164208

165209
// Validate all requested accessRecords
@@ -178,6 +222,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
178222
switch access.Action {
179223
case "push":
180224
verb = "update"
225+
pushChecks[access.Resource.Name] = struct{}{}
181226
case "pull":
182227
verb = "get"
183228
case "*":
@@ -197,7 +242,14 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
197242
verifiedPrune = true
198243
default:
199244
if err := verifyImageStreamAccess(ctx, imageStreamNS, imageStreamName, verb, osClient); err != nil {
200-
return nil, ac.wrapErr(err)
245+
if verb == "get" {
246+
possibleCrossMountErrors[access.Resource.Name] = &crossRepoMountAuthError{
247+
sourceRepoName: access.Resource.Name,
248+
err: err,
249+
}
250+
} else {
251+
return nil, ac.wrapErr(err)
252+
}
201253
}
202254
}
203255

@@ -219,6 +271,35 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
219271
}
220272
}
221273

274+
// deal with any possible cross-mount errors
275+
for namespaceAndName, err := range possibleCrossMountErrors {
276+
// If we have no push requests, this can't be a cross-mount request, so error
277+
if len(pushChecks) == 0 {
278+
return nil, err
279+
}
280+
// If we also requested a push to this ns/name, this isn't a cross-mount request, so error
281+
if _, exists := pushChecks[namespaceAndName]; exists {
282+
return nil, err
283+
}
284+
if len(pushChecks) > 1 {
285+
context.GetLogger(ctx).Warn("cannot determine cross-repo mount target from multiple push checks")
286+
continue
287+
}
288+
for target := range pushChecks {
289+
crmErr := err.(*crossRepoMountAuthError)
290+
crmErr.targetRepoName = target
291+
}
292+
}
293+
294+
// Conditionally add auth errors we want to handle later to the context
295+
if len(possibleCrossMountErrors) != 0 {
296+
context.GetLogger(ctx).Debugf("Origin auth: deferring errors: %#v", possibleCrossMountErrors)
297+
ctx = WithDeferredErrors(ctx, possibleCrossMountErrors)
298+
}
299+
300+
// Always add a marker to the context so we know auth was run
301+
ctx = WithAuthPerformed(ctx)
302+
222303
return WithUserClient(ctx, osClient), nil
223304
}
224305

@@ -272,6 +353,9 @@ func verifyOpenShiftUser(ctx context.Context, client client.UsersInterface) erro
272353
return nil
273354
}
274355

356+
// verifyImageStreamAccess returns nil if the given client is granted access to an image stream identified by
357+
// <namespace>/<imageRepo>. Otherwise the access is denied. The user embedded in given client must be able to
358+
// <verb> (get/update) imagestreams/layers resource in the <namespace> to have the access granted.
275359
func verifyImageStreamAccess(ctx context.Context, namespace, imageRepo, verb string, client client.LocalSubjectAccessReviewsNamespacer) error {
276360
sar := authorizationapi.LocalSubjectAccessReview{
277361
Action: authorizationapi.AuthorizationAttributes{
@@ -299,6 +383,9 @@ func verifyImageStreamAccess(ctx context.Context, namespace, imageRepo, verb str
299383
return nil
300384
}
301385

386+
// verifyPruneAccess returns nil if the given client is granted access to images resource. Otherwise the
387+
// access is denied. The user embedded in given client must be able to delete images resource in a cluster to
388+
// have the access granted.
302389
func verifyPruneAccess(ctx context.Context, client client.SubjectAccessReviews) error {
303390
sar := authorizationapi.SubjectAccessReview{
304391
Action: authorizationapi.AuthorizationAttributes{

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{
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/docker/distribution"
9+
"github.com/docker/distribution/context"
10+
"github.com/docker/distribution/digest"
11+
"github.com/docker/distribution/reference"
12+
"github.com/docker/distribution/registry/storage"
13+
)
14+
15+
// errorBlobStore wraps a distribution.BlobStore for a particular repo.
16+
// before delegating, it ensures auth completed and there were no errors relevant to the repo.
17+
type errorBlobStore struct {
18+
store distribution.BlobStore
19+
repo *repository
20+
}
21+
22+
var _ distribution.BlobStore = &errorBlobStore{}
23+
24+
func (r *errorBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
25+
context.GetLogger(ctx).Infof("(*errorBlobStore).Stat: starting")
26+
if err := r.repo.checkPendingErrors(ctx); err != nil {
27+
return distribution.Descriptor{}, err
28+
}
29+
return r.store.Stat(ctx, dgst)
30+
}
31+
32+
func (r *errorBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
33+
context.GetLogger(ctx).Infof("(*errorBlobStore).Get: starting")
34+
if err := r.repo.checkPendingErrors(ctx); err != nil {
35+
return nil, err
36+
}
37+
return r.store.Get(ctx, dgst)
38+
}
39+
40+
func (r *errorBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
41+
context.GetLogger(ctx).Infof("(*errorBlobStore).Open: starting")
42+
if err := r.repo.checkPendingErrors(ctx); err != nil {
43+
return nil, err
44+
}
45+
return r.store.Open(ctx, dgst)
46+
}
47+
48+
func (r *errorBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
49+
if err := r.repo.checkPendingErrors(ctx); err != nil {
50+
return distribution.Descriptor{}, err
51+
}
52+
return r.store.Put(ctx, mediaType, p)
53+
}
54+
55+
func (r *errorBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
56+
context.GetLogger(ctx).Infof("(*errorBlobStore).Create: starting with %d options", len(options))
57+
if err := r.repo.checkPendingErrors(ctx); err != nil {
58+
return nil, err
59+
}
60+
61+
options = append(options, storage.WithRepositoryMiddlewareWrapper(
62+
func(ctx context.Context, repo distribution.Repository, name reference.Named) (distribution.Repository, error) {
63+
context.GetLogger(r.repo.ctx).Infof("(*errorBlobStore).Create: called middleware wrapper function")
64+
nameParts := strings.SplitN(name.Name(), "/", 2)
65+
if len(nameParts) != 2 {
66+
return nil, fmt.Errorf("invalid repository name %q: it must be of the format <project>/<name>", repo.Named().Name())
67+
}
68+
middleware := *r.repo
69+
middleware.Repository = repo
70+
middleware.namespace = nameParts[0]
71+
middleware.name = nameParts[1]
72+
context.GetLogger(r.repo.ctx).Infof("(*errorBlobStore).Create: returning new middleware for repository=%s", middleware.Name())
73+
return &middleware, nil
74+
}))
75+
76+
return r.store.Create(ctx, options...)
77+
}
78+
79+
func (r *errorBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
80+
if err := r.repo.checkPendingErrors(ctx); err != nil {
81+
return nil, err
82+
}
83+
return r.store.Resume(ctx, id)
84+
}
85+
86+
func (r *errorBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, req *http.Request, dgst digest.Digest) error {
87+
context.GetLogger(ctx).Infof("(*errorBlobStore).ServeBlob: starting")
88+
if err := r.repo.checkPendingErrors(ctx); err != nil {
89+
return err
90+
}
91+
return r.store.ServeBlob(ctx, w, req, dgst)
92+
}
93+
94+
func (r *errorBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
95+
if err := r.repo.checkPendingErrors(ctx); err != nil {
96+
return err
97+
}
98+
return r.store.Delete(ctx, dgst)
99+
}

0 commit comments

Comments
 (0)