diff --git a/pkg/identity/keystone/authenticator_test.go b/pkg/identity/keystone/authenticator_test.go new file mode 100644 index 0000000..e16e10e --- /dev/null +++ b/pkg/identity/keystone/authenticator_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package keystone + +import ( + "os" + "reflect" + "testing" + + "github.com/gophercloud/gophercloud" + + "github.com/gophercloud/gophercloud/openstack" + "k8s.io/apiserver/pkg/authentication/user" +) + +func newKeystoneAuthenticator(authURL string, client *gophercloud.ServiceClient) KeystoneAuthenticator { + + return KeystoneAuthenticator{ + authURL: authURL, + client: client, + } + +} + +func TestAuthenticateToken(t *testing.T) { + var ( + calledWithToken []string + + resultUsers map[string]user.Info + ) + + authUrl := os.Getenv("OS_AUTH_URL") + + provider, _ := openstack.NewClient(authUrl) + cli := &gophercloud.ServiceClient{ + ProviderClient: provider, + Endpoint: authUrl, + } + + a := newKeystoneAuthenticator(authUrl, cli) + + calledWithToken, resultUsers = []string{}, nil + a.AuthenticateToken("bad1") + a.AuthenticateToken("bad2") + a.AuthenticateToken("bad3") + a.AuthenticateToken("bad1") + a.AuthenticateToken("bad2") + a.AuthenticateToken("bad3") + if !reflect.DeepEqual(calledWithToken, []string{"bad1", "bad2", "bad3", "bad1", "bad2", "bad3"}) { + t.Errorf("Expected failing calls to bypass cache, got %v", calledWithToken) + } + + // reset calls, make the backend return success for three user tokens + calledWithToken = []string{} + resultUsers = map[string]user.Info{} + resultUsers["usertoken1"] = &user.DefaultInfo{Name: "user1"} + resultUsers["usertoken2"] = &user.DefaultInfo{Name: "user2"} + resultUsers["usertoken3"] = &user.DefaultInfo{Name: "user3"} + + if user, ok, err := a.AuthenticateToken("usertoken1"); err != nil || !ok || user.GetName() != "user1" { + t.Errorf("Expected user1") + } + if user, ok, err := a.AuthenticateToken("usertoken2"); err != nil || !ok || user.GetName() != "user2" { + t.Errorf("Expected user2") + } + if user, ok, err := a.AuthenticateToken("usertoken3"); err != nil || !ok || user.GetName() != "user3" { + t.Errorf("Expected user3") + } + if !reflect.DeepEqual(calledWithToken, []string{"usertoken1", "usertoken2", "usertoken3"}) { + t.Errorf("Expected token calls, got %v", calledWithToken) + } + + // reset calls, make the backend return failures + calledWithToken = []string{} + resultUsers = nil + + // authenticate calls still succeed and backend is not hit + if user, ok, err := a.AuthenticateToken("usertoken1"); err != nil || !ok || user.GetName() != "user1" { + t.Errorf("Expected user1") + } + if user, ok, err := a.AuthenticateToken("usertoken2"); err != nil || !ok || user.GetName() != "user2" { + t.Errorf("Expected user2") + } + if user, ok, err := a.AuthenticateToken("usertoken3"); err != nil || !ok || user.GetName() != "user3" { + t.Errorf("Expected user3") + } + if !reflect.DeepEqual(calledWithToken, []string{}) { + t.Errorf("Expected no token calls, got %v", calledWithToken) + } + + a.AuthenticateToken("usertoken1") + a.AuthenticateToken("usertoken2") + a.AuthenticateToken("usertoken3") + if !reflect.DeepEqual(calledWithToken, []string{"usertoken1", "usertoken2", "usertoken3"}) { + t.Errorf("Expected token calls, got %v", calledWithToken) + } +} diff --git a/pkg/identity/keystone/authorizer.go b/pkg/identity/keystone/authorizer.go index ca92c0e..9d502c5 100644 --- a/pkg/identity/keystone/authorizer.go +++ b/pkg/identity/keystone/authorizer.go @@ -31,6 +31,15 @@ type KeystoneAuthorizer struct { pl PolicyList } +// NewAuthorizer returns a new authorizer +func NewAuthorizer(authURL string, client *gophercloud.ServiceClient, pl PolicyList) authorizer.Authorizer { + return &KeystoneAuthorizer{ + authURL: authURL, + client: client, + pl: pl, + } +} + func resourceMatches(p Policy, a authorizer.Attributes) bool { if p.NonResourceSpec != nil && p.ResourceSpec != nil { glog.Infof("Policy has both resource and nonresource sections. skipping : %#v", p) diff --git a/pkg/identity/keystone/authorizer_test.go b/pkg/identity/keystone/authorizer_test.go new file mode 100644 index 0000000..4b7bb32 --- /dev/null +++ b/pkg/identity/keystone/authorizer_test.go @@ -0,0 +1,131 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package keystone + +import ( + "testing" + + "os" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestAuthorizer(t *testing.T) { + + authUrl := os.Getenv("OS_AUTH_URL") + + provider, _ := openstack.NewClient(authUrl) + cli := &gophercloud.ServiceClient{ + ProviderClient: provider, + Endpoint: authUrl, + } + path := os.Getenv("OS_POLICY_PATH") + policy, _ := NewFromFile(path) + authz := NewAuthorizer(authUrl, cli, policy).(*KeystoneAuthorizer) + + node0 := &user.DefaultInfo{Name: "system:node:node0", Groups: []string{"system:nodes"}} + + tests := []struct { + name string + attrs authorizer.AttributesRecord + expect authorizer.Decision + }{ + { + name: "allowed configmap", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node0", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, + { + name: "allowed secret via pod", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, + { + name: "allowed shared secret via pod", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-shared", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, + { + name: "allowed shared secret via pvc", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node0-ns0", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, + { + name: "allowed pvc", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node0", Namespace: "ns0"}, + expect: authorizer.DecisionAllow, + }, + { + name: "allowed pv", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node0-ns0", Namespace: ""}, + expect: authorizer.DecisionAllow, + }, + + { + name: "disallowed configmap", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node1", Namespace: "ns0"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed secret via pod", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node1", Namespace: "ns0"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed shared secret via pvc", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node1-ns0", Namespace: "ns0"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed pvc", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed pv", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed attachment - no relationship", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "volumeattachments", APIGroup: "storage.k8s.io", Name: "attachment0-node1"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "disallowed attachment - feature disabled", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "volumeattachments", APIGroup: "storage.k8s.io", Name: "attachment0-node0"}, + expect: authorizer.DecisionNoOpinion, + }, + { + name: "allowed attachment - feature enabled", + attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "volumeattachments", APIGroup: "storage.k8s.io", Name: "attachment0-node0"}, + expect: authorizer.DecisionAllow, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + decision, _, _ := authz.Authorize(tc.attrs) + if decision != tc.expect { + t.Errorf("expected %v, got %v", tc.expect, decision) + } + }) + } +}