func formatRepositoryError(repository *importRepository, refName string, refID string, defErr error) (err error) { err = defErr switch { case isDockerError(err, v2.ErrorCodeManifestUnknown): ref := repository.Ref ref.Tag, ref.ID = refName, refID err = kapierrors.NewNotFound(api.Resource("dockerimage"), ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) } return }
// ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role. func ConfirmNoEscalation(ctx api.Context, ruleResolver AuthorizationRuleResolver, rules []rbac.PolicyRule) error { ruleResolutionErrors := []error{} ownerLocalRules, err := ruleResolver.GetEffectivePolicyRules(ctx) if err != nil { // As per AuthorizationRuleResolver contract, this may return a non fatal error with an incomplete list of policies. Log the error and continue. user, _ := api.UserFrom(ctx) glog.V(1).Infof("non-fatal error getting local rules for %v: %v", user, err) ruleResolutionErrors = append(ruleResolutionErrors, err) } masterContext := api.WithNamespace(ctx, "") ownerGlobalRules, err := ruleResolver.GetEffectivePolicyRules(masterContext) if err != nil { // Same case as above. Log error, don't fail. user, _ := api.UserFrom(ctx) glog.V(1).Infof("non-fatal error getting global rules for %v: %v", user, err) ruleResolutionErrors = append(ruleResolutionErrors, err) } ownerRules := make([]rbac.PolicyRule, 0, len(ownerGlobalRules)+len(ownerLocalRules)) ownerRules = append(ownerRules, ownerLocalRules...) ownerRules = append(ownerRules, ownerGlobalRules...) ownerRightsCover, missingRights := Covers(ownerRules, rules) if !ownerRightsCover { user, _ := api.UserFrom(ctx) return errors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v ruleResolutionErrors=%v", missingRights, user, ownerRules, ruleResolutionErrors)) } return nil }
func ConfirmNoEscalation(ctx kapi.Context, ruleResolver AuthorizationRuleResolver, role authorizationinterfaces.Role) error { ruleResolutionErrors := []error{} ownerLocalRules, err := ruleResolver.GetEffectivePolicyRules(ctx) if err != nil { // do not fail in this case. Rules are purely additive, so we can continue with a coverage check based on the rules we have user, _ := kapi.UserFrom(ctx) glog.V(1).Infof("non-fatal error getting local rules for %v: %v", user, err) ruleResolutionErrors = append(ruleResolutionErrors, err) } masterContext := kapi.WithNamespace(ctx, "") ownerGlobalRules, err := ruleResolver.GetEffectivePolicyRules(masterContext) if err != nil { // do not fail in this case. Rules are purely additive, so we can continue with a coverage check based on the rules we have user, _ := kapi.UserFrom(ctx) glog.V(1).Infof("non-fatal error getting global rules for %v: %v", user, err) ruleResolutionErrors = append(ruleResolutionErrors, err) } ownerRules := make([]authorizationapi.PolicyRule, 0, len(ownerGlobalRules)+len(ownerLocalRules)) ownerRules = append(ownerRules, ownerLocalRules...) ownerRules = append(ownerRules, ownerGlobalRules...) ownerRightsCover, missingRights := Covers(ownerRules, role.Rules()) if !ownerRightsCover { user, _ := kapi.UserFrom(ctx) return kapierrors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v ruleResolutionErrors=%v", missingRights, user, ownerRules, ruleResolutionErrors)) } return nil }
func (m *VirtualStorage) confirmNoEscalation(ctx kapi.Context, roleBinding *authorizationapi.RoleBinding) error { modifyingRole, err := m.getReferencedRole(roleBinding.RoleRef) if err != nil { return err } ruleResolver := rulevalidation.NewDefaultRuleResolver( m.PolicyRegistry, m.BindingRegistry, m.ClusterPolicyRegistry, m.ClusterPolicyBindingRegistry, ) ownerLocalRules, err := ruleResolver.GetEffectivePolicyRules(ctx) if err != nil { return kapierrors.NewInternalError(err) } masterContext := kapi.WithNamespace(ctx, "") ownerGlobalRules, err := ruleResolver.GetEffectivePolicyRules(masterContext) if err != nil { return kapierrors.NewInternalError(err) } ownerRules := make([]authorizationapi.PolicyRule, 0, len(ownerGlobalRules)+len(ownerLocalRules)) ownerRules = append(ownerRules, ownerLocalRules...) ownerRules = append(ownerRules, ownerGlobalRules...) ownerRightsCover, missingRights := rulevalidation.Covers(ownerRules, modifyingRole.Rules) if !ownerRightsCover { user, _ := kapi.UserFrom(ctx) return kapierrors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v", missingRights, user, ownerRules)) } return nil }
// ServeHTTP implements rest.HookHandler func (w *WebHook) ServeHTTP(writer http.ResponseWriter, req *http.Request, ctx kapi.Context, name, subpath string) error { parts := strings.Split(subpath, "/") if len(parts) != 2 { return errors.NewBadRequest(fmt.Sprintf("unexpected hook subpath %s", subpath)) } secret, hookType := parts[0], parts[1] plugin, ok := w.plugins[hookType] if !ok { return errors.NewNotFound(buildapi.Resource("buildconfighook"), hookType) } config, err := w.registry.GetBuildConfig(ctx, name) if err != nil { // clients should not be able to find information about build configs in // the system unless the config exists and the secret matches return errors.NewUnauthorized(fmt.Sprintf("the webhook %q for %q did not accept your secret", hookType, name)) } revision, envvars, proceed, err := plugin.Extract(config, secret, "", req) if !proceed { switch err { case webhook.ErrSecretMismatch, webhook.ErrHookNotEnabled: return errors.NewUnauthorized(fmt.Sprintf("the webhook %q for %q did not accept your secret", hookType, name)) case webhook.MethodNotSupported: return errors.NewMethodNotSupported(buildapi.Resource("buildconfighook"), req.Method) } if _, ok := err.(*errors.StatusError); !ok && err != nil { return errors.NewInternalError(fmt.Errorf("hook failed: %v", err)) } return err } warning := err buildTriggerCauses := generateBuildTriggerInfo(revision, hookType, secret) request := &buildapi.BuildRequest{ TriggeredBy: buildTriggerCauses, ObjectMeta: kapi.ObjectMeta{Name: name}, Revision: revision, Env: envvars, } if _, err := w.instantiator.Instantiate(config.Namespace, request); err != nil { return errors.NewInternalError(fmt.Errorf("could not generate a build: %v", err)) } return warning }
// ServeHTTP implements rest.HookHandler func (c *controller) ServeHTTP(w http.ResponseWriter, req *http.Request, ctx kapi.Context, name, subpath string) error { parts := strings.Split(subpath, "/") if len(parts) < 2 { return errors.NewBadRequest(fmt.Sprintf("unexpected hook subpath %s", subpath)) } secret, hookType := parts[0], parts[1] plugin, ok := c.plugins[hookType] if !ok { return errors.NewNotFound(buildapi.Resource("buildconfighook"), hookType) } config, err := c.registry.GetBuildConfig(ctx, name) if err != nil { // clients should not be able to find information about build configs in the system unless the config exists // and the secret matches return errors.NewUnauthorized(fmt.Sprintf("the webhook %q for %q did not accept your secret", hookType, name)) } revision, proceed, err := plugin.Extract(config, secret, "", req) switch err { case webhook.ErrSecretMismatch, webhook.ErrHookNotEnabled: return errors.NewUnauthorized(fmt.Sprintf("the webhook %q for %q did not accept your secret", hookType, name)) case nil: default: return errors.NewInternalError(fmt.Errorf("hook failed: %v", err)) } if !proceed { return nil } request := &buildapi.BuildRequest{ ObjectMeta: kapi.ObjectMeta{Name: name}, Revision: revision, } if _, err := c.instantiator.Instantiate(config.Namespace, request); err != nil { return errors.NewInternalError(fmt.Errorf("could not generate a build: %v", err)) } return nil }
// ConfirmNoEscalation determines if the roles for a given user in a given namespace encompass the provided role. func ConfirmNoEscalation(ctx api.Context, ruleResolver AuthorizationRuleResolver, rules []rbac.PolicyRule) error { ruleResolutionErrors := []error{} user, ok := api.UserFrom(ctx) if !ok { return fmt.Errorf("no user on context") } namespace, _ := api.NamespaceFrom(ctx) ownerRules, err := ruleResolver.RulesFor(user, namespace) if err != nil { // As per AuthorizationRuleResolver contract, this may return a non fatal error with an incomplete list of policies. Log the error and continue. glog.V(1).Infof("non-fatal error getting local rules for %v: %v", user, err) ruleResolutionErrors = append(ruleResolutionErrors, err) } ownerRightsCover, missingRights := Covers(ownerRules, rules) if !ownerRightsCover { user, _ := api.UserFrom(ctx) return apierrors.NewUnauthorized(fmt.Sprintf("attempt to grant extra privileges: %v user=%v ownerrules=%v ruleResolutionErrors=%v", missingRights, user, ownerRules, ruleResolutionErrors)) } return nil }
func (h *MultiHandler) HandleChallenge(requestURL string, headers http.Header) (http.Header, bool, error) { // If we've already selected a handler, it alone can handle all subsequent challenges (don't change horses in mid-stream) if h.handler != nil { return h.handler.HandleChallenge(requestURL, headers) } // Otherwise, filter our list of handlers to the ones that can handle this request applicable := []ChallengeHandler{} for _, handler := range h.possibleHandlers { if handler.CanHandle(headers) { applicable = append(applicable, handler) } } h.possibleHandlers = applicable // Then select the first available handler that successfully handles the request var ( retryHeaders http.Header retry bool err error ) for i, handler := range h.possibleHandlers { retryHeaders, retry, err = handler.HandleChallenge(requestURL, headers) if err != nil { glog.V(5).Infof("handler[%d] error: %v", i, err) } // If the handler successfully handled the challenge, or we have no other options, select it as our handler if err == nil || i == len(h.possibleHandlers)-1 { h.handler = handler return retryHeaders, retry, err } } return nil, false, apierrs.NewUnauthorized("unhandled challenge") }
// RequestToken uses the cmd arguments to locate an openshift oauth server and attempts to authenticate // it returns the access token if it gets one. An error if it does not func RequestToken(clientCfg *restclient.Config, reader io.Reader, defaultUsername string, defaultPassword string) (string, error) { challengeHandler := &BasicChallengeHandler{ Host: clientCfg.Host, Reader: reader, Username: defaultUsername, Password: defaultPassword, } rt, err := restclient.TransportFor(clientCfg) if err != nil { return "", err } // requestURL holds the current URL to make requests to. This can change if the server responds with a redirect requestURL := clientCfg.Host + "/oauth/authorize?response_type=token&client_id=openshift-challenging-client" // requestHeaders holds additional headers to add to the request. This can be changed by challengeHandlers requestHeaders := http.Header{} // requestedURLSet/requestedURLList hold the URLs we have requested, to prevent redirect loops. Gets reset when a challenge is handled. requestedURLSet := sets.NewString() requestedURLList := []string{} for { // Make the request resp, err := request(rt, requestURL, requestHeaders) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { if resp.Header.Get("WWW-Authenticate") != "" { if !challengeHandler.CanHandle(resp.Header) { return "", apierrs.NewUnauthorized("unhandled challenge") } // Handle a challenge newRequestHeaders, shouldRetry, err := challengeHandler.HandleChallenge(resp.Header) if err != nil { return "", err } if !shouldRetry { return "", apierrs.NewUnauthorized("challenger chose not to retry the request") } // Reset request set/list. Since we're setting different headers, it is legitimate to request the same urls requestedURLSet = sets.NewString() requestedURLList = []string{} // Use the response to the challenge as the new headers requestHeaders = newRequestHeaders continue } // Unauthorized with no challenge unauthorizedError := apierrs.NewUnauthorized("") // Attempt to read body content and include as an error detail if details, err := ioutil.ReadAll(resp.Body); err == nil && len(details) > 0 { unauthorizedError.ErrStatus.Details = &unversioned.StatusDetails{ Causes: []unversioned.StatusCause{ {Message: string(details)}, }, } } return "", unauthorizedError } if resp.StatusCode == http.StatusFound { redirectURL := resp.Header.Get("Location") // OAuth response case (access_token or error parameter) accessToken, err := oauthAuthorizeResult(redirectURL) if err != nil { return "", err } if len(accessToken) > 0 { return accessToken, err } // Non-OAuth response, just follow the URL // add to our list of redirects requestedURLList = append(requestedURLList, redirectURL) // detect loops if !requestedURLSet.Has(redirectURL) { requestedURLSet.Insert(redirectURL) requestURL = redirectURL continue } return "", apierrs.NewInternalError(fmt.Errorf("redirect loop: %s", strings.Join(requestedURLList, " -> "))) } // Unknown response return "", apierrs.NewInternalError(fmt.Errorf("unexpected response: %d", resp.StatusCode)) } }
// importRepositoryFromDocker loads the tags and images requested in the passed importRepository, obeying the // optional rate limiter. Errors are set onto the individual tags and digest objects. func (isi *ImageStreamImporter) importRepositoryFromDocker(ctx gocontext.Context, retriever RepositoryRetriever, repository *importRepository, limiter flowcontrol.RateLimiter) { glog.V(5).Infof("importing remote Docker repository registry=%s repository=%s insecure=%t", repository.Registry, repository.Name, repository.Insecure) // retrieve the repository repo, err := retriever.Repository(ctx, repository.Registry, repository.Name, repository.Insecure) if err != nil { glog.V(5).Infof("unable to access repository %#v: %#v", repository, err) switch { case err == reference.ErrReferenceInvalidFormat: err = field.Invalid(field.NewPath("from", "name"), repository.Name, "the provided repository name is not valid") case isDockerError(err, v2.ErrorCodeNameUnknown): err = kapierrors.NewNotFound(api.Resource("dockerimage"), repository.Ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.Contains(err.Error(), "tls: oversized record received with length") && !repository.Insecure: err = kapierrors.NewBadRequest("this repository is HTTP only and requires the insecure flag to import") case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q and did not have credentials to the repository", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "does not support v2 API"): importRepositoryFromDockerV1(ctx, repository, limiter) return } applyErrorToRepository(repository, err) return } // get a manifest context s, err := repo.Manifests(ctx) if err != nil { glog.V(5).Infof("unable to access manifests for repository %#v: %#v", repository, err) switch { case isDockerError(err, v2.ErrorCodeNameUnknown): err = kapierrors.NewNotFound(api.Resource("dockerimage"), repository.Ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q and did not have credentials to the repository", repository.Ref.Exact())) } applyErrorToRepository(repository, err) return } // get a blob context b := repo.Blobs(ctx) // if repository import is requested (MaximumTags), attempt to load the tags, sort them, and request the first N if count := repository.MaximumTags; count > 0 || count == -1 { tags, err := repo.Tags(ctx).All(ctx) if err != nil { glog.V(5).Infof("unable to access tags for repository %#v: %#v", repository, err) switch { case isDockerError(err, v2.ErrorCodeNameUnknown): err = kapierrors.NewNotFound(api.Resource("dockerimage"), repository.Ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) } repository.Err = err return } // some images on the Hub have empty tags - treat those as "latest" set := sets.NewString(tags...) if set.Has("") { set.Delete("") set.Insert(api.DefaultImageTag) } tags = set.List() // include only the top N tags in the result, put the rest in AdditionalTags api.PrioritizeTags(tags) for _, s := range tags { if count <= 0 && repository.MaximumTags != -1 { repository.AdditionalTags = append(repository.AdditionalTags, s) continue } count-- repository.Tags = append(repository.Tags, importTag{ Name: s, }) } } // load digests for i := range repository.Digests { importDigest := &repository.Digests[i] if importDigest.Err != nil || importDigest.Image != nil { continue } d, err := digest.ParseDigest(importDigest.Name) if err != nil { importDigest.Err = err continue } limiter.Accept() manifest, err := s.Get(ctx, d) if err != nil { glog.V(5).Infof("unable to access digest %q for repository %#v: %#v", d, repository, err) importDigest.Err = formatRepositoryError(repository, "", importDigest.Name, err) continue } if signedManifest, isSchema1 := manifest.(*schema1.SignedManifest); isSchema1 { importDigest.Image, err = schema1ToImage(signedManifest, d) } else if deserializedManifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 { imageConfig, err := b.Get(ctx, deserializedManifest.Config.Digest) if err != nil { glog.V(5).Infof("unable to access the image config using digest %q for repository %#v: %#v", d, repository, err) if isDockerError(err, v2.ErrorCodeManifestUnknown) { ref := repository.Ref ref.ID = deserializedManifest.Config.Digest.String() importDigest.Err = kapierrors.NewNotFound(api.Resource("dockerimage"), ref.Exact()) } else { importDigest.Err = formatRepositoryError(repository, "", importDigest.Name, err) } continue } importDigest.Image, err = schema2ToImage(deserializedManifest, imageConfig, d) } else { glog.V(5).Infof("unsupported manifest type: %T", manifest) continue } if err != nil { importDigest.Err = err continue } if err := api.ImageWithMetadata(importDigest.Image); err != nil { importDigest.Err = err continue } if importDigest.Image.DockerImageMetadata.Size == 0 { if err := isi.calculateImageSize(ctx, repo, importDigest.Image); err != nil { importDigest.Err = err continue } } } for i := range repository.Tags { importTag := &repository.Tags[i] if importTag.Err != nil || importTag.Image != nil { continue } limiter.Accept() desc, err := repo.Tags(ctx).Get(ctx, importTag.Name) if err != nil { glog.V(5).Infof("unable to get tag %q for repository %#v: %#v", importTag.Name, repository, err) importTag.Err = formatRepositoryError(repository, importTag.Name, "", err) continue } manifest, err := s.Get(ctx, desc.Digest) if err != nil { glog.V(5).Infof("unable to access digest %q for tag %q for repository %#v: %#v", desc.Digest, importTag.Name, repository, err) importTag.Err = formatRepositoryError(repository, importTag.Name, "", err) continue } if signedManifest, isSchema1 := manifest.(*schema1.SignedManifest); isSchema1 { importTag.Image, err = schema1ToImage(signedManifest, "") } else if deserializedManifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 { imageConfig, err := b.Get(ctx, deserializedManifest.Config.Digest) if err != nil { glog.V(5).Infof("unable to access image config using digest %q for tag %q for repository %#v: %#v", desc.Digest, importTag.Name, repository, err) importTag.Err = formatRepositoryError(repository, importTag.Name, "", err) continue } importTag.Image, err = schema2ToImage(deserializedManifest, imageConfig, "") } else { glog.V(5).Infof("unsupported manifest type: %T", manifest) continue } if err != nil { importTag.Err = err continue } if err := api.ImageWithMetadata(importTag.Image); err != nil { importTag.Err = err continue } if importTag.Image.DockerImageMetadata.Size == 0 { if err := isi.calculateImageSize(ctx, repo, importTag.Image); err != nil { importTag.Err = err continue } } } }
// TestIsErrorQuotaExceeded verifies that if a resource exceedes allowed usage, the admission will return // error we can recognize. func TestIsErrorQuotaExceeded(t *testing.T) { for _, tc := range []struct { name string err error shouldMatch bool }{ { name: "unrelated error", err: errors.New("unrelated"), }, { name: "wrong type", err: errors.New(errQuotaMessageString), }, { name: "wrong kapi type", err: kerrors.NewUnauthorized(errQuotaMessageString), }, { name: "unrelated forbidden error", err: kerrors.NewForbidden(kapi.Resource("imageStreams"), "is", errors.New("unrelated")), }, { name: "unrelated invalid error", err: kerrors.NewInvalid(imageapi.Kind("imageStreams"), "is", field.ErrorList{ field.Required(field.NewPath("imageStream").Child("Spec"), "detail"), }), }, { name: "quota error not recognized with invalid reason", err: kerrors.NewInvalid(imageapi.Kind("imageStreams"), "is", field.ErrorList{ field.Forbidden(field.NewPath("imageStreams"), errQuotaMessageString), }), }, { name: "quota unknown error not recognized with invalid reason", err: kerrors.NewInvalid(imageapi.Kind("imageStreams"), "is", field.ErrorList{ field.Forbidden(field.NewPath("imageStreams"), errQuotaUnknownMessageString), }), }, { name: "quota exceeded error", err: kerrors.NewForbidden(kapi.Resource("imageStream"), "is", errors.New(errQuotaMessageString)), shouldMatch: true, }, { name: "quota unknown error", err: kerrors.NewForbidden(kapi.Resource("imageStream"), "is", errors.New(errQuotaUnknownMessageString)), shouldMatch: true, }, { name: "limits exceeded error with forbidden reason", err: kerrors.NewForbidden(imageapi.Resource("imageStream"), "is", errors.New(errLimitsMessageString)), shouldMatch: true, }, { name: "limits exceeded error with invalid reason", err: kerrors.NewInvalid(imageapi.Kind("imageStreams"), "is", field.ErrorList{ field.Forbidden(field.NewPath("imageStream"), errLimitsMessageString), }), shouldMatch: true, }, } { match := IsErrorQuotaExceeded(tc.err) if !match && tc.shouldMatch { t.Errorf("[%s] expected to match error [%T]: %v", tc.name, tc.err, tc.err) } if match && !tc.shouldMatch { t.Errorf("[%s] expected not to match error [%T]: %v", tc.name, tc.err, tc.err) } } }
// importRepositoryFromDocker loads the tags and images requested in the passed importRepository, obeying the // optional rate limiter. Errors are set onto the individual tags and digest objects. func importRepositoryFromDocker(ctx gocontext.Context, retriever RepositoryRetriever, repository *importRepository, limiter util.RateLimiter) { // retrieve the repository repo, err := retriever.Repository(ctx, repository.Registry, repository.Name, repository.Insecure) if err != nil { glog.V(5).Infof("unable to access repository %#v: %#v", repository, err) switch { case isDockerError(err, v2.ErrorCodeNameUnknown): err = kapierrors.NewNotFound("DockerImage", repository.Ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.Contains(err.Error(), "tls: oversized record received with length") && !repository.Insecure: err = kapierrors.NewBadRequest("this repository is HTTP only and requires the insecure flag to import") case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q and did not have credentials to the repository", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "does not support v2 API"): importRepositoryFromDockerV1(ctx, repository, limiter) return } applyErrorToRepository(repository, err) return } // get a manifest context s, err := repo.Manifests(ctx) if err != nil { glog.V(5).Infof("unable to access manifests for repository %#v: %#v", repository, err) switch { case isDockerError(err, v2.ErrorCodeNameUnknown): err = kapierrors.NewNotFound("DockerImage", repository.Ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q and did not have credentials to the repository", repository.Ref.Exact())) } applyErrorToRepository(repository, err) return } // if repository import is requested (MaximumTags), attempt to load the tags, sort them, and request the first N if count := repository.MaximumTags; count > 0 || count == -1 { tags, err := s.Tags() if err != nil { glog.V(5).Infof("unable to access tags for repository %#v: %#v", repository, err) switch { case isDockerError(err, v2.ErrorCodeNameUnknown): err = kapierrors.NewNotFound("DockerImage", repository.Ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) } repository.Err = err return } // some images on the Hub have empty tags - treat those as "latest" set := sets.NewString(tags...) if set.Has("") { set.Delete("") set.Insert(api.DefaultImageTag) } tags = set.List() // include only the top N tags in the result, put the rest in AdditionalTags api.PrioritizeTags(tags) for _, s := range tags { if count <= 0 && repository.MaximumTags != -1 { repository.AdditionalTags = append(repository.AdditionalTags, s) continue } count-- repository.Tags = append(repository.Tags, importTag{ Name: s, }) } } // load digests for i := range repository.Digests { importDigest := &repository.Digests[i] if importDigest.Err != nil || importDigest.Image != nil { continue } d, err := digest.ParseDigest(importDigest.Name) if err != nil { importDigest.Err = err continue } limiter.Accept() m, err := s.Get(d) if err != nil { glog.V(5).Infof("unable to access digest %q for repository %#v: %#v", d, repository, err) switch { case isDockerError(err, v2.ErrorCodeManifestUnknown): ref := repository.Ref ref.Tag, ref.ID = "", importDigest.Name err = kapierrors.NewNotFound("DockerImage", ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) } importDigest.Err = err continue } importDigest.Image, err = schema1ToImage(m, d) if err != nil { importDigest.Err = err continue } if err := api.ImageWithMetadata(importDigest.Image); err != nil { importDigest.Err = err continue } } for i := range repository.Tags { importTag := &repository.Tags[i] if importTag.Err != nil || importTag.Image != nil { continue } limiter.Accept() m, err := s.GetByTag(importTag.Name) if err != nil { glog.V(5).Infof("unable to access tag %q for repository %#v: %#v", importTag.Name, repository, err) switch { case isDockerError(err, v2.ErrorCodeManifestUnknown): ref := repository.Ref ref.Tag = importTag.Name err = kapierrors.NewNotFound("DockerImage", ref.Exact()) case isDockerError(err, errcode.ErrorCodeUnauthorized): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) case strings.HasSuffix(err.Error(), "no basic auth credentials"): err = kapierrors.NewUnauthorized(fmt.Sprintf("you may not have access to the Docker image %q", repository.Ref.Exact())) } importTag.Err = err continue } importTag.Image, err = schema1ToImage(m, "") if err != nil { importTag.Err = err continue } if err := api.ImageWithMetadata(importTag.Image); err != nil { importTag.Err = err continue } } }
// RequestToken locates an openshift oauth server and attempts to authenticate. // It returns the access token if it gets one, or an error if it does not. // It should only be invoked once on a given RequestTokenOptions instance. // The Handler held by the options is released as part of this call. func (o *RequestTokenOptions) RequestToken() (string, error) { defer func() { // Always release the handler if err := o.Handler.Release(); err != nil { // Release errors shouldn't fail the token request, just log glog.V(4).Infof("error releasing handler: %v", err) } }() rt, err := restclient.TransportFor(o.ClientConfig) if err != nil { return "", err } // requestURL holds the current URL to make requests to. This can change if the server responds with a redirect requestURL := o.ClientConfig.Host + "/oauth/authorize?response_type=token&client_id=openshift-challenging-client" // requestHeaders holds additional headers to add to the request. This can be changed by o.Handlers requestHeaders := http.Header{} // requestedURLSet/requestedURLList hold the URLs we have requested, to prevent redirect loops. Gets reset when a challenge is handled. requestedURLSet := sets.NewString() requestedURLList := []string{} handledChallenge := false for { // Make the request resp, err := request(rt, requestURL, requestHeaders) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { if resp.Header.Get("WWW-Authenticate") != "" { if !o.Handler.CanHandle(resp.Header) { return "", apierrs.NewUnauthorized("unhandled challenge") } // Handle the challenge newRequestHeaders, shouldRetry, err := o.Handler.HandleChallenge(requestURL, resp.Header) if err != nil { return "", err } if !shouldRetry { return "", apierrs.NewUnauthorized("challenger chose not to retry the request") } // Remember if we've ever handled a challenge handledChallenge = true // Reset request set/list. Since we're setting different headers, it is legitimate to request the same urls requestedURLSet = sets.NewString() requestedURLList = []string{} // Use the response to the challenge as the new headers requestHeaders = newRequestHeaders continue } // Unauthorized with no challenge unauthorizedError := apierrs.NewUnauthorized("") // Attempt to read body content and include as an error detail if details, err := ioutil.ReadAll(resp.Body); err == nil && len(details) > 0 { unauthorizedError.ErrStatus.Details = &unversioned.StatusDetails{ Causes: []unversioned.StatusCause{ {Message: string(details)}, }, } } return "", unauthorizedError } // if we've ever handled a challenge, see if the handler also considers the interaction complete. // this is required for negotiate flows with mutual authentication. if handledChallenge { if err := o.Handler.CompleteChallenge(requestURL, resp.Header); err != nil { return "", err } } if resp.StatusCode == http.StatusFound { redirectURL := resp.Header.Get("Location") // OAuth response case (access_token or error parameter) accessToken, err := oauthAuthorizeResult(redirectURL) if err != nil { return "", err } if len(accessToken) > 0 { return accessToken, err } // Non-OAuth response, just follow the URL // add to our list of redirects requestedURLList = append(requestedURLList, redirectURL) // detect loops if !requestedURLSet.Has(redirectURL) { requestedURLSet.Insert(redirectURL) requestURL = redirectURL continue } return "", apierrs.NewInternalError(fmt.Errorf("redirect loop: %s", strings.Join(requestedURLList, " -> "))) } // Unknown response return "", apierrs.NewInternalError(fmt.Errorf("unexpected response: %d", resp.StatusCode)) } }
func TestCache(t *testing.T) { tokenAuthInvocations := []string{} tokenAuth := authenticator.TokenFunc(func(token string) (user.Info, bool, error) { tokenAuthInvocations = append(tokenAuthInvocations, token) switch { case strings.HasPrefix(token, "user"): return &user.DefaultInfo{Name: token}, true, nil case strings.HasPrefix(token, "unauthorized"): return nil, false, kerrs.NewUnauthorized(token) case strings.HasPrefix(token, "error"): return nil, false, errors.New(token) default: return nil, false, nil } }) tests := map[string]struct { TTL time.Duration CacheSize int Requests []testRequest ExpectedInvocations []string ExpectedCacheSize int }{ "miss": { TTL: time.Minute, CacheSize: 1, Requests: []testRequest{ {Token: "user1", ExpectedUserName: "******", ExpectedOK: true}, }, ExpectedInvocations: []string{"user1"}, ExpectedCacheSize: 1, }, "cache hit user": { TTL: time.Minute, CacheSize: 1, Requests: []testRequest{ {Token: "user1", ExpectedUserName: "******", ExpectedOK: true}, {Token: "user1", ExpectedUserName: "******", ExpectedOK: true}, }, ExpectedInvocations: []string{"user1"}, ExpectedCacheSize: 1, }, "cache hit invalid": { TTL: time.Minute, CacheSize: 1, Requests: []testRequest{ {Token: "invalid1", ExpectedOK: false}, {Token: "invalid1", ExpectedOK: false}, }, ExpectedInvocations: []string{"invalid1"}, ExpectedCacheSize: 1, }, "cache hit unauthorized error": { TTL: time.Minute, CacheSize: 1, Requests: []testRequest{ {Token: "unauthorized1", ExpectedErr: true}, {Token: "unauthorized1", ExpectedErr: true}, }, ExpectedInvocations: []string{"unauthorized1"}, ExpectedCacheSize: 1, }, "uncacheable error": { TTL: time.Minute, CacheSize: 1, Requests: []testRequest{ {Token: "error1", ExpectedErr: true}, {Token: "error1", ExpectedErr: true}, }, ExpectedInvocations: []string{"error1", "error1"}, ExpectedCacheSize: 0, }, "expire": { TTL: time.Minute, CacheSize: 1, Requests: []testRequest{ {Token: "user1", ExpectedUserName: "******", ExpectedOK: true}, {Token: "user1", ExpectedUserName: "******", ExpectedOK: true, Offset: 2 * time.Minute}, }, ExpectedInvocations: []string{"user1", "user1"}, ExpectedCacheSize: 1, }, "evacuation": { TTL: time.Minute, CacheSize: 2, Requests: []testRequest{ // Request user1 {Token: "user1", ExpectedUserName: "******", ExpectedOK: true}, // Requests for user2 and user3 evacuate user1 {Token: "user2", ExpectedUserName: "******", ExpectedOK: true, Offset: 10 * time.Second}, {Token: "user3", ExpectedUserName: "******", ExpectedOK: true, Offset: 20 * time.Second}, {Token: "user2", ExpectedUserName: "******", ExpectedOK: true, Offset: 30 * time.Second}, {Token: "user3", ExpectedUserName: "******", ExpectedOK: true, Offset: 40 * time.Second}, {Token: "user2", ExpectedUserName: "******", ExpectedOK: true, Offset: 50 * time.Second}, {Token: "user3", ExpectedUserName: "******", ExpectedOK: true, Offset: 60 * time.Second}, // Request for user1 refetches {Token: "user1", ExpectedUserName: "******", ExpectedOK: true}, }, ExpectedInvocations: []string{"user1", "user2", "user3", "user1"}, ExpectedCacheSize: 2, }, } for k, tc := range tests { tokenAuthInvocations = []string{} start := time.Now() auth, err := NewAuthenticator(tokenAuth, tc.TTL, tc.CacheSize) if err != nil { t.Errorf("%s: Unexpected error: %v", k, err) } cacheAuth := auth.(*CacheAuthenticator) for i, r := range tc.Requests { cacheAuth.now = func() time.Time { return start.Add(r.Offset) } u, ok, err := cacheAuth.AuthenticateToken(r.Token) if r.ExpectedErr != (err != nil) { t.Errorf("%s: %d: Expected err=%v, got %v", k, i, r.ExpectedErr, err) continue } if ok != r.ExpectedOK { t.Errorf("%s: %d: Expected ok=%v, got %v", k, i, r.ExpectedOK, ok) continue } if ok && u.GetName() != r.ExpectedUserName { t.Errorf("%s: %d: Expected username=%v, got %v", k, i, r.ExpectedUserName, u.GetName()) continue } } if !reflect.DeepEqual(tc.ExpectedInvocations, tokenAuthInvocations) { t.Errorf("%s: Expected invocations=%v, got %v", k, tc.ExpectedInvocations, tokenAuthInvocations) } if cacheAuth.cache.Len() != tc.ExpectedCacheSize { t.Errorf("%s: Expected cache size %d, got %d", k, tc.ExpectedCacheSize, cacheAuth.cache.Len()) } } }