Keystone authentication plugin

This commit is contained in:
Ruddarraju, Uday Kumar Raju 2015-07-23 23:06:14 -07:00
parent c367d3c2e5
commit 937db3f70d
7 changed files with 261 additions and 2 deletions

View File

@ -85,6 +85,7 @@ type APIServer struct {
TokenAuthFile string
ServiceAccountKeyFile string
ServiceAccountLookup bool
KeystoneURL string
AuthorizationMode string
AuthorizationPolicyFile string
AdmissionControl string
@ -188,6 +189,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin")
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
@ -334,7 +336,7 @@ func (s *APIServer) Run(_ []string) error {
glog.Warning("no RSA key provided, service account token authentication disabled")
}
}
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, etcdStorage)
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, etcdStorage, s.KeystoneURL)
if err != nil {
glog.Fatalf("Invalid Authentication Config: %v", err)
}

View File

@ -64,6 +64,15 @@ and is a csv file with 3 columns: password, user name, user id.
When using basic authentication from an http client, the apiserver expects an `Authorization` header
with a value of `Basic BASE64ENCODED(USER:PASSWORD)`.
**Keystone authentication** is enabled by passing the `--experimental-keystone-url=<AuthURL>`
option to the apiserver during startup. The plugin is implemented in
`plugin/pkg/auth/authenticator/request/keystone/keystone.go`.
For details on how to use keystone to manage projects and users, refer to the
[Keystone documentation](http://docs.openstack.org/developer/keystone/). Please note that
this plugin is still experimental which means it is subject to changes.
Please refer to the [discussion](https://github.com/GoogleCloudPlatform/kubernetes/pull/11798#issuecomment-129655212)
and the [blueprint](https://github.com/GoogleCloudPlatform/kubernetes/issues/11626) for more details
## Plugin Development
We plan for the Kubernetes API server to issue tokens

View File

@ -80,6 +80,7 @@ executor-cgroup-prefix
executor-logv
executor-path
executor-suicide-timeout
experimental-keystone-url
experimental-prefix
external-hostname
failover-timeout

View File

@ -26,13 +26,14 @@ import (
"k8s.io/kubernetes/pkg/util"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/keystone"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/union"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/tokenfile"
)
// NewAuthenticator returns an authenticator.Request or an error
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, storage storage.Interface) (authenticator.Request, error) {
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, storage storage.Interface, keystoneURL string) (authenticator.Request, error) {
var authenticators []authenticator.Request
if len(basicAuthFile) > 0 {
@ -67,6 +68,14 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyF
authenticators = append(authenticators, serviceAccountAuth)
}
if len(keystoneURL) > 0 {
keystoneAuth, err := newAuthenticatorFromKeystoneURL(keystoneURL)
if err != nil {
return nil, err
}
authenticators = append(authenticators, keystoneAuth)
}
switch len(authenticators) {
case 0:
return nil, nil
@ -133,3 +142,13 @@ func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Reques
return x509.New(opts, x509.CommonNameUserConversion), nil
}
// newAuthenticatorFromTokenFile returns an authenticator.Request or an error
func newAuthenticatorFromKeystoneURL(keystoneConfigFile string) (authenticator.Request, error) {
keystoneAuthenticator, err := keystone.NewKeystoneAuthenticator(keystoneConfigFile)
if err != nil {
return nil, err
}
return basicauth.New(keystoneAuthenticator), nil
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 provide authentication via keystone.
// For details //about keystone and how to use the plugin, refer to
// https://github.com/GoogleCloudPlatform/kubernetes/blob/oidc/docs/admin/authentication.md
package keystone

View File

@ -0,0 +1,61 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 (
"errors"
"strings"
"github.com/golang/glog"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
"k8s.io/kubernetes/pkg/auth/user"
)
// Keystone authenticator contacts openstack keystone to validate user's credentials passed in the request.
// The keystone endpoint is passed during apiserver startup
type KeystoneAuthenticator struct {
authURL string
}
func (keystoneAuthenticator *KeystoneAuthenticator) AuthenticatePassword(username string, password string) (user.Info, bool, error) {
opts := gophercloud.AuthOptions{
IdentityEndpoint: keystoneAuthenticator.authURL,
Username: username,
Password: password,
}
_, err := openstack.AuthenticatedClient(opts)
if err != nil {
glog.Info("Failed: Starting openstack authenticate client")
return nil, false, errors.New("Failed to authenticate")
}
return &user.DefaultInfo{Name: username}, true, nil
}
// New returns a request authenticator that validates credentials using openstack keystone
func NewKeystoneAuthenticator(authURL string) (*KeystoneAuthenticator, error) {
if !strings.HasPrefix(authURL, "https") {
return nil, errors.New("Auth URL should be secure and start with https")
}
if authURL == "" {
return nil, errors.New("Auth URL is empty")
}
return &KeystoneAuthenticator{authURL}, nil
}

View File

@ -0,0 +1,147 @@
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 (
"encoding/base64"
"net/http"
"testing"
"k8s.io/kubernetes/pkg/auth/user"
"k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
)
type testKeystoneAuthenticator struct {
User user.Info
OK bool
Err error
}
func (osClient *testKeystoneAuthenticator) AuthenticatePassword(username string, password string) (user.Info, bool, error) {
userPasswordMap := map[string]string{
"user1": "password1",
"user2": "password2",
"user3": "password3",
"user4": "password4",
"user5": "password5",
"user6": "password6",
"user7": "password7",
"user8": "password8",
"user9": "password9",
}
if userPasswordMap[username] == password {
return &user.DefaultInfo{Name: username}, true, nil
}
return nil, false, nil
}
func TestKeystoneAuth(t *testing.T) {
testCases := map[string]struct {
Header string
keystoneAuthenticator testKeystoneAuthenticator
ExpectedCalled bool
ExpectedUsername string
ExpectedPassword string
ExpectedUser string
ExpectedOK bool
ExpectedErr bool
}{
"no header": {
Header: "",
},
"non-basic header": {
Header: "Bearer foo",
},
"empty value basic header": {
Header: "Basic",
},
"whitespace value basic header": {
Header: "Basic ",
},
"non base-64 basic header": {
Header: "Basic !@#$",
ExpectedErr: true,
},
"malformed basic header": {
Header: "Basic " + base64.StdEncoding.EncodeToString([]byte("user_without_password")),
ExpectedErr: true,
},
"empty password basic header": {
Header: "Basic " + base64.StdEncoding.EncodeToString([]byte("user1:")),
ExpectedOK: false,
},
"valid basic header": {
Header: "Basic " + base64.StdEncoding.EncodeToString([]byte("user1:password1:withcolon")),
ExpectedOK: false,
ExpectedErr: false,
},
"password auth returned user": {
Header: "Basic " + base64.StdEncoding.EncodeToString([]byte("user1:password1")),
ExpectedCalled: true,
ExpectedUsername: "user1",
ExpectedPassword: "password1",
ExpectedOK: true,
},
"password auth returned error": {
Header: "Basic " + base64.StdEncoding.EncodeToString([]byte("user1:password2")),
ExpectedCalled: true,
ExpectedUsername: "user1",
ExpectedPassword: "password1",
ExpectedErr: false,
ExpectedOK: false,
},
}
for k, testCase := range testCases {
ksAuth := testCase.keystoneAuthenticator
auth := basicauth.New(&ksAuth)
req, _ := http.NewRequest("GET", "/", nil)
if testCase.Header != "" {
req.Header.Set("Authorization", testCase.Header)
}
user, ok, err := auth.AuthenticateRequest(req)
if testCase.ExpectedErr && err == nil {
t.Errorf("%s: Expected error, got none", k)
continue
}
if !testCase.ExpectedErr && err != nil {
t.Errorf("%s: Did not expect error, got err:%v", k, err)
continue
}
if testCase.ExpectedOK != ok {
t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectedOK, ok)
continue
}
if testCase.ExpectedOK {
if testCase.ExpectedUsername != user.GetName() {
t.Errorf("%s: Expected user.name=%v, got %v", k, testCase.ExpectedUsername, user.GetName())
continue
}
}
}
}