Example #1
0
// ReadPluginConfig will read a plugin configuration object from a reader stream
func ReadPluginConfig(reader io.Reader, config runtime.Object) error {
	if reader == nil || reflect.ValueOf(reader).IsNil() {
		return nil
	}

	configBytes, err := ioutil.ReadAll(reader)
	if err != nil {
		return err
	}
	err = configlatest.ReadYAML(configBytes, config)
	if err != nil {
		return err
	}
	return nil
}
Example #2
0
func ReadConfig(configFile io.Reader) (*api.ClusterResourceOverrideConfig, error) {
	obj, err := configlatest.ReadYAML(configFile)
	if err != nil {
		glog.V(5).Infof("%s error reading config: %v", api.PluginName, err)
		return nil, err
	}
	if obj == nil {
		return nil, nil
	}
	config, ok := obj.(*api.ClusterResourceOverrideConfig)
	if !ok {
		return nil, fmt.Errorf("unexpected config object: %#v", obj)
	}
	glog.V(5).Infof("%s config is: %v", api.PluginName, config)
	return config, nil
}
Example #3
0
func readConfig(reader io.Reader) (*api.PodNodeConstraintsConfig, error) {
	if reader == nil || reflect.ValueOf(reader).IsNil() {
		return nil, nil
	}
	obj, err := configlatest.ReadYAML(reader)
	if err != nil {
		return nil, err
	}
	if obj == nil {
		return nil, nil
	}
	config, ok := obj.(*api.PodNodeConstraintsConfig)
	if !ok {
		return nil, fmt.Errorf("unexpected config object: %#v", obj)
	}
	// No validation needed since config is just list of strings
	return config, nil
}
Example #4
0
func readConfig(reader io.Reader) (*requestlimitapi.ProjectRequestLimitConfig, error) {
	obj, err := configlatest.ReadYAML(reader)
	if err != nil {
		return nil, err
	}
	if obj == nil {
		return nil, nil
	}
	config, ok := obj.(*requestlimitapi.ProjectRequestLimitConfig)
	if !ok {
		return nil, fmt.Errorf("unexpected config object: %#v", obj)
	}
	errs := requestlimitapivalidation.ValidateProjectRequestLimitConfig(config)
	if len(errs) > 0 {
		return nil, errs.ToAggregate()
	}
	return config, nil
}
Example #5
0
func readConfig(reader io.Reader) (*api.RunOnceDurationConfig, error) {
	obj, err := configlatest.ReadYAML(reader)
	if err != nil {
		return nil, err
	}
	if obj == nil {
		return nil, nil
	}
	config, ok := obj.(*api.RunOnceDurationConfig)
	if !ok {
		return nil, fmt.Errorf("unexpected config object %#v", obj)
	}
	errs := validation.ValidateRunOnceDurationConfig(config)
	if len(errs) > 0 {
		return nil, errs.ToAggregate()
	}
	return config, nil
}
Example #6
0
func readConfig(reader io.Reader) (*api.RunOnceDurationConfig, error) {
	config := &api.RunOnceDurationConfig{}
	if reader == nil || reflect.ValueOf(reader).IsNil() {
		return config, nil
	}
	configBytes, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}
	err = configlatest.ReadYAML(configBytes, config)
	if err != nil {
		return nil, err
	}
	errs := validation.ValidateRunOnceDurationConfig(config)
	if len(errs) > 0 {
		return nil, errs.ToAggregate()
	}
	return config, nil
}
Example #7
0
func ReadConfig(configFile io.Reader) (*api.ClusterResourceOverrideConfig, error) {
	if configFile == nil || reflect.ValueOf(configFile).IsNil() /* pointer to nil */ {
		glog.V(5).Infof("%s has no config to read.", api.PluginName)
		return nil, nil
	}
	configBytes, err := ioutil.ReadAll(configFile)
	if err != nil {
		return nil, err
	}

	config := &api.ClusterResourceOverrideConfig{}
	err = configlatest.ReadYAML(configBytes, config)
	if err != nil {
		glog.V(5).Infof("%s error reading config: %v", api.PluginName, err)
		return nil, err
	}
	glog.V(5).Infof("%s config is: %v", api.PluginName, config)
	return config, nil
}
Example #8
0
func readConfig(reader io.Reader) (*ProjectRequestLimitConfig, error) {
	if reader == nil || reflect.ValueOf(reader).IsNil() {
		return &ProjectRequestLimitConfig{}, nil
	}

	configBytes, err := ioutil.ReadAll(reader)
	if err != nil {
		return nil, err
	}
	config := &ProjectRequestLimitConfig{}
	err = configlatest.ReadYAML(configBytes, config)
	if err != nil {
		return nil, err
	}
	errs := ValidateProjectRequestLimitConfig(config)
	if len(errs) > 0 {
		return nil, errs.ToAggregate()
	}
	return config, nil
}
Example #9
0
func init() {
	admission.RegisterPlugin(api.PluginName, func(client clientset.Interface, input io.Reader) (admission.Interface, error) {
		obj, err := configlatest.ReadYAML(input)
		if err != nil {
			return nil, err
		}
		if obj == nil {
			return nil, nil
		}
		config, ok := obj.(*api.ImagePolicyConfig)
		if !ok {
			return nil, fmt.Errorf("unexpected config object: %#v", obj)
		}
		if errs := validation.Validate(config); len(errs) > 0 {
			return nil, errs.ToAggregate()
		}
		glog.V(5).Infof("%s admission controller loaded with config: %#v", api.PluginName, config)
		return newImagePolicyPlugin(client, config)
	})
}
Example #10
0
func TestDefaultPolicy(t *testing.T) {
	input, err := os.Open("api/v1/default-policy.yaml")
	if err != nil {
		t.Fatal(err)
	}
	obj, err := configlatest.ReadYAML(input)
	if err != nil {
		t.Fatal(err)
	}
	if obj == nil {
		t.Fatal(obj)
	}
	config, ok := obj.(*api.ImagePolicyConfig)
	if !ok {
		t.Fatal(config)
	}
	if errs := validation.Validate(config); len(errs) > 0 {
		t.Fatal(errs.ToAggregate())
	}

	plugin, err := newImagePolicyPlugin(nil, config)
	if err != nil {
		t.Fatal(err)
	}

	goodImage := &imageapi.Image{
		ObjectMeta:           kapi.ObjectMeta{Name: "sha256:good"},
		DockerImageReference: "integrated.registry/goodns/goodimage:good",
	}
	badImage := &imageapi.Image{
		ObjectMeta: kapi.ObjectMeta{
			Name: "sha256:bad",
			Annotations: map[string]string{
				"images.openshift.io/deny-execution": "true",
			},
		},
		DockerImageReference: "integrated.registry/badns/badimage:bad",
	}

	client := testclient.NewSimpleFake(
		goodImage,
		badImage,

		// respond to image stream tag in this order:
		&unversioned.Status{
			Reason: unversioned.StatusReasonNotFound,
			Code:   404,
			Details: &unversioned.StatusDetails{
				Kind: "ImageStreamTag",
			},
		},
		&imageapi.ImageStreamTag{
			ObjectMeta: kapi.ObjectMeta{Name: "mysql:goodtag", Namespace: "repo"},
			Image:      *goodImage,
		},
		&imageapi.ImageStreamTag{
			ObjectMeta: kapi.ObjectMeta{Name: "mysql:badtag", Namespace: "repo"},
			Image:      *badImage,
		},
	)

	store := setDefaultCache(plugin)
	plugin.SetOpenshiftClient(client)
	plugin.SetDefaultRegistryFunc(func() (string, bool) {
		return "integrated.registry", true
	})
	if err := plugin.Validate(); err != nil {
		t.Fatal(err)
	}

	originalNowFn := now
	defer (func() { now = originalNowFn })()
	now = func() time.Time { return time.Unix(1, 0) }

	// should allow a non-integrated image
	attrs := admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql:latest"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// should resolve the non-integrated image and allow it
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// should resolve the integrated image by digest and allow it
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql@sha256:good"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// should attempt resolve the integrated image by tag and fail because tag not found
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql:missingtag"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// should attempt resolve the integrated image by tag and allow it
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql:goodtag"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// should attempt resolve the integrated image by tag and forbid it
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "integrated.registry/repo/mysql:badtag"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	t.Logf("%#v", plugin.accepter)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}

	// should reject the non-integrated image due to the annotation
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:bad"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}

	// should reject the non-integrated image due to the annotation on an init container
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{InitContainers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:bad"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}

	// should reject the non-integrated image due to the annotation for a build
	attrs = admission.NewAttributesRecord(
		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Source: buildapi.BuildSource{Images: []buildapi.ImageSource{
			{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"}},
		}}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}
	attrs = admission.NewAttributesRecord(
		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Strategy: buildapi.BuildStrategy{DockerStrategy: &buildapi.DockerBuildStrategy{
			From: &kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"},
		}}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}
	attrs = admission.NewAttributesRecord(
		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Strategy: buildapi.BuildStrategy{SourceStrategy: &buildapi.SourceBuildStrategy{
			From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"},
		}}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}
	attrs = admission.NewAttributesRecord(
		&buildapi.Build{Spec: buildapi.BuildSpec{CommonSpec: buildapi.CommonSpec{Strategy: buildapi.BuildStrategy{CustomStrategy: &buildapi.CustomBuildStrategy{
			From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"},
		}}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Build"},
		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "builds"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}

	// should allow the non-integrated image due to the annotation for a build config because it's not in the list, even though it has
	// a valid spec
	attrs = admission.NewAttributesRecord(
		&buildapi.BuildConfig{Spec: buildapi.BuildConfigSpec{CommonSpec: buildapi.CommonSpec{Source: buildapi.BuildSource{Images: []buildapi.ImageSource{
			{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/mysql@sha256:bad"}},
		}}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "BuildConfig"},
		"default", "build1", unversioned.GroupVersionResource{Version: "v1", Resource: "buildconfigs"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// should hit the cache on the previously good image and continue to allow it (the copy in cache was previously safe)
	goodImage.Annotations = map[string]string{"images.openshift.io/deny-execution": "true"}
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}

	// moving 2 minutes in the future should bypass the cache and deny the image
	now = func() time.Time { return time.Unix(1, 0).Add(2 * time.Minute) }
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err == nil || !apierrs.IsInvalid(err) {
		t.Fatal(err)
	}

	// setting a namespace annotation should allow the rule to be skipped immediately
	store.Add(&kapi.Namespace{
		ObjectMeta: kapi.ObjectMeta{
			Namespace: "",
			Name:      "default",
			Annotations: map[string]string{
				api.IgnorePolicyRulesAnnotation: "execution-denied",
			},
		},
	})
	attrs = admission.NewAttributesRecord(
		&kapi.Pod{Spec: kapi.PodSpec{Containers: []kapi.Container{{Image: "index.docker.io/mysql@sha256:good"}}}},
		nil, unversioned.GroupVersionKind{Version: "v1", Kind: "Pod"},
		"default", "pod1", unversioned.GroupVersionResource{Version: "v1", Resource: "pods"},
		"", admission.Create, nil,
	)
	if err := plugin.Admit(attrs); err != nil {
		t.Fatal(err)
	}
}
Example #11
0
func TestOAuthLDAP(t *testing.T) {
	var (
		randomSuffix = string(kutil.NewUUID())

		providerName = "myldapprovider"

		bindDN       = "uid=admin,ou=company,ou=" + randomSuffix
		bindPassword = "******" + randomSuffix

		searchDN     = "ou=company,ou=" + randomSuffix
		searchAttr   = "myuid" + randomSuffix
		searchScope  = "one"              // must be "one","sub", or "base"
		searchFilter = "(myAttr=myValue)" // must be a valid LDAP filter format

		nameAttr1  = "missing-name-attr"
		nameAttr2  = "a-display-name" + randomSuffix
		idAttr1    = "missing-id-attr"
		idAttr2    = "dn" // "dn" is a special value, so don't add a random suffix to make sure we handle it correctly
		emailAttr1 = "missing-attr"
		emailAttr2 = "c-mail" + randomSuffix
		loginAttr1 = "missing-attr"
		loginAttr2 = "d-mylogin" + randomSuffix

		myUserUID      = "myuser"
		myUserName     = "******"
		myUserEmail    = "*****@*****.**"
		myUserDN       = searchAttr + "=" + myUserUID + "," + searchDN
		myUserPassword = "******" + randomSuffix
	)

	expectedAttributes := [][]byte{}
	for _, attr := range sets.NewString(searchAttr, nameAttr1, nameAttr2, idAttr1, idAttr2, emailAttr1, emailAttr2, loginAttr1, loginAttr2).List() {
		expectedAttributes = append(expectedAttributes, []byte(attr))
	}
	expectedSearchRequest := ldapserver.SearchRequest{
		BaseObject:   []byte(searchDN),
		Scope:        ldapserver.SearchRequestSingleLevel,
		DerefAliases: 0,
		SizeLimit:    2,
		TimeLimit:    0,
		TypesOnly:    false,
		Attributes:   expectedAttributes,
		Filter:       fmt.Sprintf("(&%s(%s=%s))", searchFilter, searchAttr, myUserUID),
	}

	// Start LDAP server
	ldapAddress, err := testserver.FindAvailableBindAddress(8389, 8400)
	if err != nil {
		t.Fatalf("could not allocate LDAP bind address: %v", err)
	}
	ldapServer := testutil.NewTestLDAPServer()
	ldapServer.SetPassword(bindDN, bindPassword)
	ldapServer.Start(ldapAddress)
	defer ldapServer.Stop()

	testutil.RequireEtcd(t)
	defer testutil.DumpEtcdOnFailure(t)
	masterOptions, err := testserver.DefaultMasterOptions()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Generate an encrypted file/keyfile to contain the bindPassword
	bindPasswordFile, err := ioutil.TempFile("", "bindPassword")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	defer os.Remove(bindPasswordFile.Name())
	bindPasswordKeyFile, err := ioutil.TempFile("", "bindPasswordKey")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	defer os.Remove(bindPasswordKeyFile.Name())
	encryptOpts := &admin.EncryptOptions{
		CleartextData: []byte(bindPassword),
		EncryptedFile: bindPasswordFile.Name(),
		GenKeyFile:    bindPasswordKeyFile.Name(),
	}
	if err := encryptOpts.Encrypt(); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	masterOptions.OAuthConfig.IdentityProviders[0] = configapi.IdentityProvider{
		Name:            providerName,
		UseAsChallenger: true,
		UseAsLogin:      true,
		MappingMethod:   "claim",
		Provider: &configapi.LDAPPasswordIdentityProvider{
			URL:    fmt.Sprintf("ldap://%s/%s?%s?%s?%s", ldapAddress, searchDN, searchAttr, searchScope, searchFilter),
			BindDN: bindDN,
			BindPassword: configapi.StringSource{
				StringSourceSpec: configapi.StringSourceSpec{
					File:    bindPasswordFile.Name(),
					KeyFile: bindPasswordKeyFile.Name(),
				},
			},
			Insecure: true,
			CA:       "",
			Attributes: configapi.LDAPAttributeMapping{
				ID:                []string{idAttr1, idAttr2},
				PreferredUsername: []string{loginAttr1, loginAttr2},
				Name:              []string{nameAttr1, nameAttr2},
				Email:             []string{emailAttr1, emailAttr2},
			},
		},
	}

	// serialize to YAML to make sure a complex StringSource survives a round-trip
	serializedOptions, err := configapilatest.WriteYAML(masterOptions)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// read back in
	deserializedObject, err := configapilatest.ReadYAML(bytes.NewBuffer(serializedOptions))
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	// assert type and proceed, using the deserialized version as our config
	if deserializedOptions, ok := deserializedObject.(*configapi.MasterConfig); !ok {
		t.Fatalf("unexpected object: %v", deserializedObject)
	} else {
		masterOptions = deserializedOptions
	}

	clusterAdminKubeConfig, err := testserver.StartConfiguredMaster(masterOptions)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	clusterAdminClient, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	// Use the server and CA info
	anonConfig := restclient.Config{}
	anonConfig.Host = clusterAdminClientConfig.Host
	anonConfig.CAFile = clusterAdminClientConfig.CAFile
	anonConfig.CAData = clusterAdminClientConfig.CAData

	// Make sure we can't authenticate as a missing user
	ldapServer.ResetRequests()
	if _, err := tokencmd.RequestToken(&anonConfig, nil, myUserUID, myUserPassword); err == nil {
		t.Error("Expected error, got none")
	}
	if len(ldapServer.BindRequests) != 1 {
		t.Errorf("Expected a single bind request for the search phase, got %d:\n%#v", len(ldapServer.BindRequests), ldapServer.BindRequests)
	}
	if len(ldapServer.SearchRequests) != 1 {
		t.Errorf("Expected a single search request, got %d:\n%#v", len(ldapServer.BindRequests), ldapServer.BindRequests)
	}

	// Add user
	ldapServer.SetPassword(myUserDN, myUserPassword)
	ldapServer.AddSearchResult(myUserDN, map[string]string{emailAttr2: myUserEmail, nameAttr2: myUserName, loginAttr2: myUserUID})

	// Make sure we can't authenticate with a bad password
	ldapServer.ResetRequests()
	if _, err := tokencmd.RequestToken(&anonConfig, nil, myUserUID, "badpassword"); err == nil {
		t.Error("Expected error, got none")
	}
	if len(ldapServer.BindRequests) != 2 {
		t.Errorf("Expected a bind request for the search phase and a failed bind request for the auth phase, got %d:\n%#v", len(ldapServer.BindRequests), ldapServer.BindRequests)
	}
	if len(ldapServer.SearchRequests) != 1 {
		t.Errorf("Expected a single search request, got %d:\n%#v", len(ldapServer.BindRequests), ldapServer.BindRequests)
	}

	// Make sure we can get a token with a good password
	ldapServer.ResetRequests()
	accessToken, err := tokencmd.RequestToken(&anonConfig, nil, myUserUID, myUserPassword)
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	if len(accessToken) == 0 {
		t.Errorf("Expected access token, got none")
	}
	if len(ldapServer.BindRequests) != 2 {
		t.Errorf("Expected a bind request for the search phase and a failed bind request for the auth phase, got %d:\n%#v", len(ldapServer.BindRequests), ldapServer.BindRequests)
	}
	if len(ldapServer.SearchRequests) != 1 {
		t.Errorf("Expected a single search request, got %d:\n%#v", len(ldapServer.BindRequests), ldapServer.BindRequests)
	}
	if !reflect.DeepEqual(expectedSearchRequest.BaseObject, ldapServer.SearchRequests[0].BaseObject) {
		t.Errorf("Expected search base DN\n\t%#v\ngot\n\t%#v",
			string(expectedSearchRequest.BaseObject),
			string(ldapServer.SearchRequests[0].BaseObject),
		)
	}
	if !reflect.DeepEqual(expectedSearchRequest.Filter, ldapServer.SearchRequests[0].Filter) {
		t.Errorf("Expected search filter\n\t%#v\ngot\n\t%#v",
			string(expectedSearchRequest.Filter),
			string(ldapServer.SearchRequests[0].Filter),
		)
	}
	{
		expectedAttrs := []string{}
		for _, a := range expectedSearchRequest.Attributes {
			expectedAttrs = append(expectedAttrs, string(a))
		}
		actualAttrs := []string{}
		for _, a := range ldapServer.SearchRequests[0].Attributes {
			actualAttrs = append(actualAttrs, string(a))
		}
		if !reflect.DeepEqual(expectedAttrs, actualAttrs) {
			t.Errorf("Expected search attributes\n\t%#v\ngot\n\t%#v", expectedAttrs, actualAttrs)
		}
	}

	// Make sure we can use the token, and it represents who we expect
	userConfig := anonConfig
	userConfig.BearerToken = accessToken
	userClient, err := client.New(&userConfig)
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}

	user, err := userClient.Users().Get("~")
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	if user.Name != myUserUID {
		t.Fatalf("Expected %s as the user, got %v", myUserUID, user)
	}

	// Make sure the identity got created and contained the mapped attributes
	identity, err := clusterAdminClient.Identities().Get(fmt.Sprintf("%s:%s", providerName, myUserDN))
	if err != nil {
		t.Fatalf("Unexpected error: %v", err)
	}
	if identity.ProviderUserName != myUserDN {
		t.Errorf("Expected %q, got %q", myUserDN, identity.ProviderUserName)
	}
	if v := identity.Extra[authapi.IdentityDisplayNameKey]; v != myUserName {
		t.Errorf("Expected %q, got %q", myUserName, v)
	}
	if v := identity.Extra[authapi.IdentityPreferredUsernameKey]; v != myUserUID {
		t.Errorf("Expected %q, got %q", myUserUID, v)
	}
	if v := identity.Extra[authapi.IdentityEmailKey]; v != myUserEmail {
		t.Errorf("Expected %q, got %q", myUserEmail, v)
	}

}