Skip to content

Commit c805ff4

Browse files
author
OpenShift Bot
authored
Merge pull request #9593 from liggitt/registry-deferred-auth-error
Merged by openshift-bot
2 parents 5ab9334 + 860fa7f commit c805ff4

File tree

7 files changed

+344
-16
lines changed

7 files changed

+344
-16
lines changed

Godeps/_workspace/src/github.com/docker/distribution/registry/storage/linkedblobstore.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/dockerregistry/server/auth.go

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

24+
type deferredErrors map[string]error
25+
26+
func (d deferredErrors) Add(namespace string, name string, err error) {
27+
d[namespace+"/"+name] = err
28+
}
29+
func (d deferredErrors) Get(namespace string, name string) (error, bool) {
30+
err, exists := d[namespace+"/"+name]
31+
return err, exists
32+
}
33+
func (d deferredErrors) Empty() bool {
34+
return len(d) == 0
35+
}
36+
2437
// DefaultRegistryClient is exposed for testing the registry with fake client.
2538
var DefaultRegistryClient = NewRegistryClient(clientcmd.NewConfig().BindToFile())
2639

@@ -61,6 +74,27 @@ func UserClientFrom(ctx context.Context) (client.Interface, bool) {
6174
return userClient, ok
6275
}
6376

77+
const authPerformedKey = "openshift.auth.performed"
78+
79+
func WithAuthPerformed(parent context.Context) context.Context {
80+
return context.WithValue(parent, authPerformedKey, true)
81+
}
82+
83+
func AuthPerformed(ctx context.Context) bool {
84+
authPerformed, ok := ctx.Value(authPerformedKey).(bool)
85+
return ok && authPerformed
86+
}
87+
88+
const deferredErrorsKey = "openshift.auth.deferredErrors"
89+
90+
func WithDeferredErrors(parent context.Context, errs deferredErrors) context.Context {
91+
return context.WithValue(parent, deferredErrorsKey, errs)
92+
}
93+
func DeferredErrorsFrom(ctx context.Context) (deferredErrors, bool) {
94+
errs, ok := ctx.Value(deferredErrorsKey).(deferredErrors)
95+
return errs, ok
96+
}
97+
6498
type AccessController struct {
6599
realm string
66100
config restclient.Config
@@ -160,6 +194,11 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
160194
}
161195
}
162196

197+
// pushChecks remembers which ns/name pairs had push access checks done
198+
pushChecks := map[string]bool{}
199+
// possibleCrossMountErrors holds errors which may be related to cross mount errors
200+
possibleCrossMountErrors := deferredErrors{}
201+
163202
verifiedPrune := false
164203

165204
// Validate all requested accessRecords
@@ -178,6 +217,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
178217
switch access.Action {
179218
case "push":
180219
verb = "update"
220+
pushChecks[imageStreamNS+"/"+imageStreamName] = true
181221
case "pull":
182222
verb = "get"
183223
case "*":
@@ -197,7 +237,11 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
197237
verifiedPrune = true
198238
default:
199239
if err := verifyImageStreamAccess(ctx, imageStreamNS, imageStreamName, verb, osClient); err != nil {
200-
return nil, ac.wrapErr(err)
240+
if access.Action == "pull" {
241+
possibleCrossMountErrors.Add(imageStreamNS, imageStreamName, ac.wrapErr(err))
242+
} else {
243+
return nil, ac.wrapErr(err)
244+
}
201245
}
202246
}
203247

@@ -219,6 +263,26 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
219263
}
220264
}
221265

266+
// deal with any possible cross-mount errors
267+
for namespaceAndName, err := range possibleCrossMountErrors {
268+
// If we have no push requests, this can't be a cross-mount request, so error
269+
if len(pushChecks) == 0 {
270+
return nil, err
271+
}
272+
// If we also requested a push to this ns/name, this isn't a cross-mount request, so error
273+
if pushChecks[namespaceAndName] {
274+
return nil, err
275+
}
276+
}
277+
278+
// Conditionally add auth errors we want to handle later to the context
279+
if !possibleCrossMountErrors.Empty() {
280+
context.GetLogger(ctx).Debugf("Origin auth: deferring errors: %#v", possibleCrossMountErrors)
281+
ctx = WithDeferredErrors(ctx, possibleCrossMountErrors)
282+
}
283+
// Always add a marker to the context so we know auth was run
284+
ctx = WithAuthPerformed(ctx)
285+
222286
return WithUserClient(ctx, osClient), nil
223287
}
224288

pkg/dockerregistry/server/auth_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ func TestAccessController(t *testing.T) {
9595
openshiftResponses []response
9696
expectedError error
9797
expectedChallenge bool
98+
expectedRepoErr string
9899
expectedActions []string
99100
}{
100101
"no token": {
@@ -203,7 +204,7 @@ func TestAccessController(t *testing.T) {
203204
{Resource: auth.Resource{Type: "repository", Name: "foo/aaa"}, Action: "pull"},
204205
{Resource: auth.Resource{Type: "repository", Name: "bar/bbb"}, Action: "push"},
205206
{Resource: auth.Resource{Type: "admin"}, Action: "prune"},
206-
{Resource: auth.Resource{Type: "repository", Name: "baz/ccc"}, Action: "pull"},
207+
{Resource: auth.Resource{Type: "repository", Name: "baz/ccc"}, Action: "push"},
207208
},
208209
basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=",
209210
openshiftResponses: []response{
@@ -221,6 +222,30 @@ func TestAccessController(t *testing.T) {
221222
"POST /oapi/v1/namespaces/baz/localsubjectaccessreviews",
222223
},
223224
},
225+
"deferred cross-mount error": {
226+
// cross-mount push requests check pull/push access on the target repo and pull access on the source repo.
227+
// we expect the access check failure for fromrepo/bbb to be added to the context as a deferred error,
228+
// which our blobstore will look for and prevent a cross mount from.
229+
access: []auth.Access{
230+
{Resource: auth.Resource{Type: "repository", Name: "pushrepo/aaa"}, Action: "pull"},
231+
{Resource: auth.Resource{Type: "repository", Name: "pushrepo/aaa"}, Action: "push"},
232+
{Resource: auth.Resource{Type: "repository", Name: "fromrepo/bbb"}, Action: "pull"},
233+
},
234+
basicToken: "b3BlbnNoaWZ0OmF3ZXNvbWU=",
235+
openshiftResponses: []response{
236+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "pushrepo", Allowed: true, Reason: "authorized!"})},
237+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "pushrepo", Allowed: true, Reason: "authorized!"})},
238+
{200, runtime.EncodeOrDie(kapi.Codecs.LegacyCodec(registered.GroupOrDie(kapi.GroupName).GroupVersions[0]), &api.SubjectAccessReviewResponse{Namespace: "fromrepo", Allowed: false, Reason: "no!"})},
239+
},
240+
expectedError: nil,
241+
expectedChallenge: false,
242+
expectedRepoErr: "fromrepo/bbb",
243+
expectedActions: []string{
244+
"POST /oapi/v1/namespaces/pushrepo/localsubjectaccessreviews",
245+
"POST /oapi/v1/namespaces/pushrepo/localsubjectaccessreviews",
246+
"POST /oapi/v1/namespaces/fromrepo/localsubjectaccessreviews",
247+
},
248+
},
224249
"valid openshift token": {
225250
access: []auth.Access{{
226251
Resource: auth.Resource{
@@ -309,6 +334,22 @@ func TestAccessController(t *testing.T) {
309334
t.Errorf("%s: expected auth context but got nil", k)
310335
continue
311336
}
337+
if !AuthPerformed(authCtx) {
338+
t.Errorf("%s: expected AuthPerformed to be true", k)
339+
continue
340+
}
341+
deferredErrors, hasDeferred := DeferredErrorsFrom(authCtx)
342+
if len(test.expectedRepoErr) > 0 {
343+
if !hasDeferred || deferredErrors[test.expectedRepoErr] == nil {
344+
t.Errorf("%s: expected deferred error for repo %s, got none", k, test.expectedRepoErr)
345+
continue
346+
}
347+
} else {
348+
if hasDeferred && len(deferredErrors) > 0 {
349+
t.Errorf("%s: didn't expect deferred errors, got %#v", k, deferredErrors)
350+
continue
351+
}
352+
}
312353
} else {
313354
_, isChallenge := err.(auth.Challenge)
314355
if test.expectedChallenge != isChallenge {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/docker/distribution"
8+
"github.com/docker/distribution/context"
9+
"github.com/docker/distribution/digest"
10+
"github.com/docker/distribution/registry/storage"
11+
)
12+
13+
// errorBlobStore wraps a distribution.BlobStore for a particular repo.
14+
// before delegating, it ensures auth completed and there were no errors relevant to the repo.
15+
type errorBlobStore struct {
16+
store distribution.BlobStore
17+
repo *repository
18+
}
19+
20+
var _ distribution.BlobStore = &errorBlobStore{}
21+
22+
func (r *errorBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
23+
if err := r.repo.checkPendingErrors(ctx); err != nil {
24+
return distribution.Descriptor{}, err
25+
}
26+
return r.store.Stat(ctx, dgst)
27+
}
28+
29+
func (r *errorBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
30+
if err := r.repo.checkPendingErrors(ctx); err != nil {
31+
return nil, err
32+
}
33+
return r.store.Get(ctx, dgst)
34+
}
35+
36+
func (r *errorBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
37+
if err := r.repo.checkPendingErrors(ctx); err != nil {
38+
return nil, err
39+
}
40+
return r.store.Open(ctx, dgst)
41+
}
42+
43+
func (r *errorBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
44+
if err := r.repo.checkPendingErrors(ctx); err != nil {
45+
return distribution.Descriptor{}, err
46+
}
47+
return r.store.Put(ctx, mediaType, p)
48+
}
49+
50+
func (r *errorBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
51+
if err := r.repo.checkPendingErrors(ctx); err != nil {
52+
return nil, err
53+
}
54+
55+
opts, err := effectiveCreateOptions(options)
56+
if err != nil {
57+
return nil, err
58+
}
59+
if err := checkPendingCrossMountErrors(ctx, opts); err != nil {
60+
context.GetLogger(r.repo.ctx).Debugf("disabling cross-mount because of pending error: %v", err)
61+
options = append(options, guardCreateOptions{DisableCrossMount: true})
62+
} else {
63+
options = append(options, guardCreateOptions{})
64+
}
65+
66+
return r.store.Create(ctx, options...)
67+
}
68+
69+
func (r *errorBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
70+
if err := r.repo.checkPendingErrors(ctx); err != nil {
71+
return nil, err
72+
}
73+
return r.store.Resume(ctx, id)
74+
}
75+
76+
func (r *errorBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, req *http.Request, dgst digest.Digest) error {
77+
if err := r.repo.checkPendingErrors(ctx); err != nil {
78+
return err
79+
}
80+
return r.store.ServeBlob(ctx, w, req, dgst)
81+
}
82+
83+
func (r *errorBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
84+
if err := r.repo.checkPendingErrors(ctx); err != nil {
85+
return err
86+
}
87+
return r.store.Delete(ctx, dgst)
88+
}
89+
90+
// Find out what the blob creation options are going to do by dry-running them
91+
func effectiveCreateOptions(options []distribution.BlobCreateOption) (*storage.CreateOptions, error) {
92+
opts := &storage.CreateOptions{}
93+
for _, createOptions := range options {
94+
err := createOptions.Apply(opts)
95+
if err != nil {
96+
return nil, err
97+
}
98+
}
99+
return opts, nil
100+
}
101+
102+
func checkPendingCrossMountErrors(ctx context.Context, opts *storage.CreateOptions) error {
103+
if !opts.Mount.ShouldMount {
104+
return nil
105+
}
106+
namespace, name, err := getNamespaceName(opts.Mount.From.Name())
107+
if err != nil {
108+
return err
109+
}
110+
return checkPendingErrors(context.GetLogger(ctx), ctx, namespace, name)
111+
}
112+
113+
// guardCreateOptions ensures the expected options type is passed, and optionally disables cross mounting
114+
type guardCreateOptions struct {
115+
DisableCrossMount bool
116+
}
117+
118+
var _ distribution.BlobCreateOption = guardCreateOptions{}
119+
120+
func (f guardCreateOptions) Apply(v interface{}) error {
121+
opts, ok := v.(*storage.CreateOptions)
122+
if !ok {
123+
return fmt.Errorf("Unexpected create options: %#v", v)
124+
}
125+
if f.DisableCrossMount {
126+
opts.Mount.ShouldMount = false
127+
}
128+
return nil
129+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package server
2+
3+
import (
4+
"github.com/docker/distribution"
5+
"github.com/docker/distribution/context"
6+
)
7+
8+
// errorTagService wraps a distribution.TagService for a particular repo.
9+
// before delegating, it ensures auth completed and there were no errors relevant to the repo.
10+
type errorTagService struct {
11+
tags distribution.TagService
12+
repo *repository
13+
}
14+
15+
var _ distribution.TagService = &errorTagService{}
16+
17+
func (t *errorTagService) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
18+
if err := t.repo.checkPendingErrors(ctx); err != nil {
19+
return distribution.Descriptor{}, err
20+
}
21+
return t.tags.Get(ctx, tag)
22+
}
23+
24+
func (t *errorTagService) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
25+
if err := t.repo.checkPendingErrors(ctx); err != nil {
26+
return err
27+
}
28+
return t.tags.Tag(ctx, tag, desc)
29+
}
30+
31+
func (t *errorTagService) Untag(ctx context.Context, tag string) error {
32+
if err := t.repo.checkPendingErrors(ctx); err != nil {
33+
return err
34+
}
35+
return t.tags.Untag(ctx, tag)
36+
}
37+
38+
func (t *errorTagService) All(ctx context.Context) ([]string, error) {
39+
if err := t.repo.checkPendingErrors(ctx); err != nil {
40+
return nil, err
41+
}
42+
return t.tags.All(ctx)
43+
}
44+
45+
func (t *errorTagService) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) {
46+
if err := t.repo.checkPendingErrors(ctx); err != nil {
47+
return nil, err
48+
}
49+
return t.tags.Lookup(ctx, digest)
50+
}

0 commit comments

Comments
 (0)