Squashed 'k8smain/' content from commit 719158d
git-subtree-dir: k8smain git-subtree-split: 719158d2c8788c30cc9e1c51bbebb53c5801524e
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
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 authenticator contains implementations for pkg/auth/authenticator interfaces
|
||||
package authenticator
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 allow
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/auth/authenticator"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
type allowAuthenticator struct{}
|
||||
|
||||
// NewAllow returns a password authenticator that allows any non-empty username
|
||||
func NewAllow() authenticator.Password {
|
||||
return allowAuthenticator{}
|
||||
}
|
||||
|
||||
// AuthenticatePassword implements authenticator.Password to allow any non-empty username,
|
||||
// using the specified username as the name and UID
|
||||
func (allowAuthenticator) AuthenticatePassword(username, password string) (user.Info, bool, error) {
|
||||
if username == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &user.DefaultInfo{Name: username, UID: username}, true, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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 allow
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAllowEmpty(t *testing.T) {
|
||||
allow := NewAllow()
|
||||
user, ok, err := allow.AuthenticatePassword("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("Unexpected success")
|
||||
}
|
||||
if user != nil {
|
||||
t.Fatalf("Unexpected user: %v", user)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowPresent(t *testing.T) {
|
||||
allow := NewAllow()
|
||||
user, ok, err := allow.AuthenticatePassword("myuser", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("Unexpected failure")
|
||||
}
|
||||
if user.GetName() != "myuser" || user.GetUID() != "myuser" {
|
||||
t.Fatalf("Unexpected user name or uid: %v", user)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
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 password contains authenticator.Password implementations
|
||||
package password
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
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 passwordfile
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
type PasswordAuthenticator struct {
|
||||
users map[string]*userPasswordInfo
|
||||
}
|
||||
|
||||
type userPasswordInfo struct {
|
||||
info *user.DefaultInfo
|
||||
password string
|
||||
}
|
||||
|
||||
// NewCSV returns a PasswordAuthenticator, populated from a CSV file.
|
||||
// The CSV file must contain records in the format "password,username,useruid"
|
||||
func NewCSV(path string) (*PasswordAuthenticator, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
users := make(map[string]*userPasswordInfo)
|
||||
reader := csv.NewReader(file)
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(record) < 3 {
|
||||
return nil, fmt.Errorf("password file '%s' must have at least 3 columns (password, user name, user uid), found %d", path, len(record))
|
||||
}
|
||||
obj := &userPasswordInfo{
|
||||
info: &user.DefaultInfo{Name: record[1], UID: record[2]},
|
||||
password: record[0],
|
||||
}
|
||||
users[obj.info.Name] = obj
|
||||
}
|
||||
|
||||
return &PasswordAuthenticator{users}, nil
|
||||
}
|
||||
|
||||
func (a *PasswordAuthenticator) AuthenticatePassword(username, password string) (user.Info, bool, error) {
|
||||
user, ok := a.users[username]
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
if user.password != password {
|
||||
return nil, false, nil
|
||||
}
|
||||
return user.info, true, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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 passwordfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
func TestPasswordFile(t *testing.T) {
|
||||
auth, err := newWithContents(t, `
|
||||
password1,user1,uid1
|
||||
password2,user2,uid2
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read passwordfile: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Username string
|
||||
Password string
|
||||
User *user.DefaultInfo
|
||||
Ok bool
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
Username: "user1",
|
||||
Password: "password1",
|
||||
User: &user.DefaultInfo{Name: "user1", UID: "uid1"},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Username: "user2",
|
||||
Password: "password2",
|
||||
User: &user.DefaultInfo{Name: "user2", UID: "uid2"},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Username: "user1",
|
||||
Password: "password2",
|
||||
},
|
||||
{
|
||||
Username: "user2",
|
||||
Password: "password1",
|
||||
},
|
||||
{
|
||||
Username: "user3",
|
||||
Password: "password3",
|
||||
},
|
||||
{
|
||||
Username: "user4",
|
||||
Password: "password4",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
user, ok, err := auth.AuthenticatePassword(testCase.Username, testCase.Password)
|
||||
if err != nil {
|
||||
t.Errorf("%d: unexpected error: %v", i, err)
|
||||
}
|
||||
if testCase.User == nil {
|
||||
if user != nil {
|
||||
t.Errorf("%d: unexpected non-nil user %#v", i, user)
|
||||
}
|
||||
} else if !reflect.DeepEqual(testCase.User, user) {
|
||||
t.Errorf("%d: expected user %#v, got %#v", i, testCase.User, user)
|
||||
}
|
||||
if testCase.Ok != ok {
|
||||
t.Errorf("%d: expected auth %v, got %v", i, testCase.Ok, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadPasswordFile(t *testing.T) {
|
||||
if _, err := newWithContents(t, `
|
||||
password1,user1,uid1
|
||||
password2,user2,uid2
|
||||
password3,user3
|
||||
password4
|
||||
`); err == nil {
|
||||
t.Fatalf("unexpected non error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsufficientColumnsPasswordFile(t *testing.T) {
|
||||
if _, err := newWithContents(t, "password4\n"); err == nil {
|
||||
t.Fatalf("unexpected non error")
|
||||
}
|
||||
}
|
||||
|
||||
func newWithContents(t *testing.T, contents string) (auth *PasswordAuthenticator, err error) {
|
||||
f, err := ioutil.TempFile("", "passwordfile_test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating passwordfile: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil {
|
||||
t.Fatalf("unexpected error writing passwordfile: %v", err)
|
||||
}
|
||||
|
||||
return NewCSV(f.Name())
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 basicauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/authenticator"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
// Authenticator authenticates requests using basic auth
|
||||
type Authenticator struct {
|
||||
auth authenticator.Password
|
||||
}
|
||||
|
||||
// New returns a request authenticator that validates credentials using the provided password authenticator
|
||||
func New(auth authenticator.Password) *Authenticator {
|
||||
return &Authenticator{auth}
|
||||
}
|
||||
|
||||
// AuthenticateRequest authenticates the request using the "Authorization: Basic" header in the request
|
||||
func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
|
||||
username, password, found := req.BasicAuth()
|
||||
if !found {
|
||||
return nil, false, nil
|
||||
}
|
||||
return a.auth.AuthenticatePassword(username, password)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
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 basicauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/authenticator"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
type testPassword struct {
|
||||
Username string
|
||||
Password string
|
||||
Called bool
|
||||
|
||||
User user.Info
|
||||
OK bool
|
||||
Err error
|
||||
}
|
||||
|
||||
func (t *testPassword) AuthenticatePassword(user, password string) (user.Info, bool, error) {
|
||||
t.Called = true
|
||||
t.Username = user
|
||||
t.Password = password
|
||||
return t.User, t.OK, t.Err
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Header string
|
||||
Password testPassword
|
||||
|
||||
ExpectedCalled bool
|
||||
ExpectedUsername string
|
||||
ExpectedPassword string
|
||||
|
||||
ExpectedUser string
|
||||
ExpectedOK bool
|
||||
ExpectedErr bool
|
||||
}{
|
||||
"no auth": {},
|
||||
"empty password basic header": {
|
||||
ExpectedCalled: true,
|
||||
ExpectedUsername: "user_with_empty_password",
|
||||
ExpectedPassword: "",
|
||||
},
|
||||
"valid basic header": {
|
||||
ExpectedCalled: true,
|
||||
ExpectedUsername: "myuser",
|
||||
ExpectedPassword: "mypassword:withcolon",
|
||||
},
|
||||
"password auth returned user": {
|
||||
Password: testPassword{User: &user.DefaultInfo{Name: "returneduser"}, OK: true},
|
||||
ExpectedCalled: true,
|
||||
ExpectedUsername: "myuser",
|
||||
ExpectedPassword: "mypw",
|
||||
ExpectedUser: "returneduser",
|
||||
ExpectedOK: true,
|
||||
},
|
||||
"password auth returned error": {
|
||||
Password: testPassword{Err: errors.New("auth error")},
|
||||
ExpectedCalled: true,
|
||||
ExpectedUsername: "myuser",
|
||||
ExpectedPassword: "mypw",
|
||||
ExpectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, testCase := range testCases {
|
||||
password := testCase.Password
|
||||
auth := authenticator.Request(New(&password))
|
||||
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
if testCase.ExpectedUsername != "" || testCase.ExpectedPassword != "" {
|
||||
req.SetBasicAuth(testCase.ExpectedUsername, testCase.ExpectedPassword)
|
||||
}
|
||||
|
||||
user, ok, err := auth.AuthenticateRequest(req)
|
||||
|
||||
if testCase.ExpectedCalled != password.Called {
|
||||
t.Errorf("%s: Expected called=%v, got %v", k, testCase.ExpectedCalled, password.Called)
|
||||
continue
|
||||
}
|
||||
if testCase.ExpectedUsername != password.Username {
|
||||
t.Errorf("%s: Expected called with username=%v, got %v", k, testCase.ExpectedUsername, password.Username)
|
||||
continue
|
||||
}
|
||||
if testCase.ExpectedPassword != password.Password {
|
||||
t.Errorf("%s: Expected called with password=%v, got %v", k, testCase.ExpectedPassword, password.Password)
|
||||
continue
|
||||
}
|
||||
|
||||
if testCase.ExpectedErr != (err != nil) {
|
||||
t.Errorf("%s: Expected err=%v, got err=%v", k, testCase.ExpectedErr, err)
|
||||
continue
|
||||
}
|
||||
if testCase.ExpectedOK != ok {
|
||||
t.Errorf("%s: Expected ok=%v, got ok=%v", k, testCase.ExpectedOK, ok)
|
||||
continue
|
||||
}
|
||||
if testCase.ExpectedUser != "" && testCase.ExpectedUser != user.GetName() {
|
||||
t.Errorf("%s: Expected user.GetName()=%v, got %v", k, testCase.ExpectedUser, user.GetName())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/kubernetes/kubernetes/blob/oidc/docs/admin/authentication.md
|
||||
package keystone
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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 union
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/authenticator"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
utilerrors "k8s.io/kubernetes/pkg/util/errors"
|
||||
)
|
||||
|
||||
// unionAuthRequestHandler authenticates requests using a chain of authenticator.Requests
|
||||
type unionAuthRequestHandler []authenticator.Request
|
||||
|
||||
// New returns a request authenticator that validates credentials using a chain of authenticator.Request objects
|
||||
func New(authRequestHandlers ...authenticator.Request) authenticator.Request {
|
||||
return unionAuthRequestHandler(authRequestHandlers)
|
||||
}
|
||||
|
||||
// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects. The first
|
||||
// success returns that identity. Errors are only returned if no matches are found.
|
||||
func (authHandler unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
|
||||
var errlist []error
|
||||
for _, currAuthRequestHandler := range authHandler {
|
||||
info, ok, err := currAuthRequestHandler.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
errlist = append(errlist, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ok {
|
||||
return info, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, utilerrors.NewAggregate(errlist)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
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 union
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
type mockAuthRequestHandler struct {
|
||||
returnUser user.Info
|
||||
isAuthenticated bool
|
||||
err error
|
||||
}
|
||||
|
||||
var (
|
||||
user1 = &user.DefaultInfo{Name: "fresh_ferret", UID: "alfa"}
|
||||
user2 = &user.DefaultInfo{Name: "elegant_sheep", UID: "bravo"}
|
||||
)
|
||||
|
||||
func (mock *mockAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
|
||||
return mock.returnUser, mock.isAuthenticated, mock.err
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestSecondPasses(t *testing.T) {
|
||||
handler1 := &mockAuthRequestHandler{returnUser: user1}
|
||||
handler2 := &mockAuthRequestHandler{returnUser: user2, isAuthenticated: true}
|
||||
authRequestHandler := New(handler1, handler2)
|
||||
req, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !isAuthenticated {
|
||||
t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated)
|
||||
}
|
||||
if !reflect.DeepEqual(user2, authenticatedUser) {
|
||||
t.Errorf("Expected %v, got %v", user2, authenticatedUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestFirstPasses(t *testing.T) {
|
||||
handler1 := &mockAuthRequestHandler{returnUser: user1, isAuthenticated: true}
|
||||
handler2 := &mockAuthRequestHandler{returnUser: user2}
|
||||
authRequestHandler := New(handler1, handler2)
|
||||
req, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !isAuthenticated {
|
||||
t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated)
|
||||
}
|
||||
if !reflect.DeepEqual(user1, authenticatedUser) {
|
||||
t.Errorf("Expected %v, got %v", user1, authenticatedUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestSuppressUnnecessaryErrors(t *testing.T) {
|
||||
handler1 := &mockAuthRequestHandler{err: errors.New("first")}
|
||||
handler2 := &mockAuthRequestHandler{isAuthenticated: true}
|
||||
authRequestHandler := New(handler1, handler2)
|
||||
req, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
_, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if !isAuthenticated {
|
||||
t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestNoAuthenticators(t *testing.T) {
|
||||
authRequestHandler := New()
|
||||
req, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if isAuthenticated {
|
||||
t.Errorf("Unexpectedly authenticated: %v", isAuthenticated)
|
||||
}
|
||||
if authenticatedUser != nil {
|
||||
t.Errorf("Unexpected authenticatedUser: %v", authenticatedUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestNonePass(t *testing.T) {
|
||||
handler1 := &mockAuthRequestHandler{}
|
||||
handler2 := &mockAuthRequestHandler{}
|
||||
authRequestHandler := New(handler1, handler2)
|
||||
req, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
_, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if isAuthenticated {
|
||||
t.Errorf("Unexpectedly authenticated: %v", isAuthenticated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestAdditiveErrors(t *testing.T) {
|
||||
handler1 := &mockAuthRequestHandler{err: errors.New("first")}
|
||||
handler2 := &mockAuthRequestHandler{err: errors.New("second")}
|
||||
authRequestHandler := New(handler1, handler2)
|
||||
req, _ := http.NewRequest("GET", "http://example.org", nil)
|
||||
|
||||
_, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req)
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "first") {
|
||||
t.Errorf("Expected error containing %v, got %v", "first", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "second") {
|
||||
t.Errorf("Expected error containing %v, got %v", "second", err)
|
||||
}
|
||||
if isAuthenticated {
|
||||
t.Errorf("Unexpectedly authenticated: %v", isAuthenticated)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
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 x509 provides a request authenticator that validates and
|
||||
// extracts user information from client certificates
|
||||
package x509
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
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 x509
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
utilerrors "k8s.io/kubernetes/pkg/util/errors"
|
||||
)
|
||||
|
||||
// UserConversion defines an interface for extracting user info from a client certificate chain
|
||||
type UserConversion interface {
|
||||
User(chain []*x509.Certificate) (user.Info, bool, error)
|
||||
}
|
||||
|
||||
// UserConversionFunc is a function that implements the UserConversion interface.
|
||||
type UserConversionFunc func(chain []*x509.Certificate) (user.Info, bool, error)
|
||||
|
||||
// User implements x509.UserConversion
|
||||
func (f UserConversionFunc) User(chain []*x509.Certificate) (user.Info, bool, error) {
|
||||
return f(chain)
|
||||
}
|
||||
|
||||
// Authenticator implements request.Authenticator by extracting user info from verified client certificates
|
||||
type Authenticator struct {
|
||||
opts x509.VerifyOptions
|
||||
user UserConversion
|
||||
}
|
||||
|
||||
// New returns a request.Authenticator that verifies client certificates using the provided
|
||||
// VerifyOptions, and converts valid certificate chains into user.Info using the provided UserConversion
|
||||
func New(opts x509.VerifyOptions, user UserConversion) *Authenticator {
|
||||
return &Authenticator{opts, user}
|
||||
}
|
||||
|
||||
// AuthenticateRequest authenticates the request using presented client certificates
|
||||
func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
|
||||
if req.TLS == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
var errlist []error
|
||||
for _, cert := range req.TLS.PeerCertificates {
|
||||
chains, err := cert.Verify(a.opts)
|
||||
if err != nil {
|
||||
errlist = append(errlist, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, chain := range chains {
|
||||
user, ok, err := a.user.User(chain)
|
||||
if err != nil {
|
||||
errlist = append(errlist, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if ok {
|
||||
return user, ok, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false, utilerrors.NewAggregate(errlist)
|
||||
}
|
||||
|
||||
// DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time,
|
||||
// and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth)
|
||||
func DefaultVerifyOptions() x509.VerifyOptions {
|
||||
return x509.VerifyOptions{
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
}
|
||||
|
||||
// CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName
|
||||
var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) {
|
||||
if len(chain[0].Subject.CommonName) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &user.DefaultInfo{Name: chain[0].Subject.CommonName}, true, nil
|
||||
})
|
||||
|
||||
// DNSNameUserConversion builds user info from a certificate chain using the first DNSName on the certificate
|
||||
var DNSNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) {
|
||||
if len(chain[0].DNSNames) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &user.DefaultInfo{Name: chain[0].DNSNames[0]}, true, nil
|
||||
})
|
||||
|
||||
// EmailAddressUserConversion builds user info from a certificate chain using the first EmailAddress on the certificate
|
||||
var EmailAddressUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) {
|
||||
if len(chain[0].EmailAddresses) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &user.DefaultInfo{Name: chain[0].EmailAddresses[0]}, true, nil
|
||||
})
|
||||
@@ -0,0 +1,575 @@
|
||||
/*
|
||||
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 x509
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
const (
|
||||
rootCACert = `-----BEGIN CERTIFICATE-----
|
||||
MIIDOTCCAqKgAwIBAgIJAOoObf5kuGgZMA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV
|
||||
BAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0G
|
||||
A1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQDEwdST09UIENB
|
||||
MB4XDTE0MTIwODIwMjU1N1oXDTI0MTIwNTIwMjU1N1owZzELMAkGA1UEBhMCVVMx
|
||||
ETAPBgNVBAgTCE15IFN0YXRlMRAwDgYDVQQHEwdNeSBDaXR5MQ8wDQYDVQQKEwZN
|
||||
eSBPcmcxEDAOBgNVBAsTB015IFVuaXQxEDAOBgNVBAMTB1JPT1QgQ0EwgZ8wDQYJ
|
||||
KoZIhvcNAQEBBQADgY0AMIGJAoGBAMfcayGpuF4vwrP8SXKDMCTJ9HV1cvb1NYEc
|
||||
UgKF0RtcWpK+i0jvhcEs0TPDZIwLSwFw6UMEt5xy4LUlv1K/SHGY3Ym3m/TXMnB9
|
||||
gkfrbWlY9LBIm4oVXwrPWyNIe74qAh1Oi03J1492uUPdHhcEmf01RIP6IIqIDuDL
|
||||
xNNggeIrAgMBAAGjgewwgekwHQYDVR0OBBYEFD3w9zA9O+s6VWj69UPJx6zhPxB4
|
||||
MIGZBgNVHSMEgZEwgY6AFD3w9zA9O+s6VWj69UPJx6zhPxB4oWukaTBnMQswCQYD
|
||||
VQQGEwJVUzERMA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzAN
|
||||
BgNVBAoTBk15IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBD
|
||||
QYIJAOoObf5kuGgZMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMBEGCWCGSAGG
|
||||
+EIBAQQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQBSrJjMevHUgBKkjaSyeKhOqd8V
|
||||
XlbA//N/mtJTD3eD/HUZBgyMcBH+sk6hnO8N9ICHtndkTrCElME9N3JA+wg2fHLW
|
||||
Lj09yrFm7u/0Wd+lcnBnczzoMDhlOjyVqsgIMhisFEw1VVaMoHblYnzY0B+oKNnu
|
||||
H9oc7u5zhTGXeV8WPg==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
selfSignedCert = `-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAnygAwIBAgIJAMaPaFbGgJN+MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNV
|
||||
BAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0G
|
||||
A1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MQ4wDAYDVQQDEwVzZWxmMTAe
|
||||
Fw0xNDEyMDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGUxCzAJBgNVBAYTAlVTMREw
|
||||
DwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkg
|
||||
T3JnMRAwDgYDVQQLEwdNeSBVbml0MQ4wDAYDVQQDEwVzZWxmMTCBnzANBgkqhkiG
|
||||
9w0BAQEFAAOBjQAwgYkCgYEA2NAe5AE//Uccy/HSqr4TBhzSe4QD5NYOWuTSKVeX
|
||||
LLJ0IK2SD3PfnFM/Y0wERx6ORZPGxM0ByPO1RgZe14uFSPEdnD2WTx4lcALK9Jci
|
||||
IrsvGRyMH0ZT6Q+35ScchAOdOJJYcvXEWf/heZauogzNQAGskwZdYxQB4zwC/es/
|
||||
EE0CAwEAAaOByjCBxzAdBgNVHQ4EFgQUfKsCqEU/sCgvcZFSonHu2UArQ3EwgZcG
|
||||
A1UdIwSBjzCBjIAUfKsCqEU/sCgvcZFSonHu2UArQ3GhaaRnMGUxCzAJBgNVBAYT
|
||||
AlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UE
|
||||
ChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MQ4wDAYDVQQDEwVzZWxmMYIJAMaP
|
||||
aFbGgJN+MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAxpo9Nyp4d3TT
|
||||
FnEC4erqQGgbc15fOF47J7bgXxsKK8o8oR/CzQ+08KhoDn3WgV39rEfX2jENDdWp
|
||||
ze3kOoP+iWSmTySHMSKVMppp0Xnls6t38mrsXtPuY8fGD2GS6VllaizMqc3wShNK
|
||||
4HADGF3q5z8hZYSV9ICQYHu5T9meF8M=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
clientCNCert = `Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 1 (0x1)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA
|
||||
Validity
|
||||
Not Before: Dec 8 20:25:58 2014 GMT
|
||||
Not After : Dec 5 20:25:58 2024 GMT
|
||||
Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=client_cn
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:a5:30:b3:2b:c0:bd:cb:29:cf:e2:d8:fd:68:b0:
|
||||
03:c3:a6:3b:1b:ec:36:73:a1:52:5d:27:ee:02:35:
|
||||
5c:51:ed:3d:3b:54:d7:11:f5:38:94:ee:fd:cc:0c:
|
||||
22:a8:f8:8e:11:2f:7c:43:5a:aa:07:3f:95:4f:50:
|
||||
22:7d:aa:e2:5d:2a:90:3d:02:1a:5b:d2:cf:3f:fb:
|
||||
dc:58:32:c5:ce:2f:81:58:31:20:eb:35:d3:53:d3:
|
||||
42:47:c2:13:68:93:62:58:b6:46:60:48:17:df:d2:
|
||||
8c:c3:40:47:cf:67:ea:27:0f:09:78:e9:d5:2a:64:
|
||||
1e:c4:33:5a:d6:0d:7a:79:93
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
Netscape Comment:
|
||||
OpenSSL Generated Certificate
|
||||
X509v3 Subject Key Identifier:
|
||||
E7:FB:1F:45:F0:71:77:AF:8C:10:4A:0A:42:03:F5:1F:1F:07:CF:DF
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78
|
||||
DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA
|
||||
serial:EA:0E:6D:FE:64:B8:68:19
|
||||
|
||||
X509v3 Subject Alternative Name:
|
||||
<EMPTY>
|
||||
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Client Authentication
|
||||
Netscape Cert Type:
|
||||
SSL Client
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
08:bc:b4:80:a5:3b:be:9a:78:f9:47:3f:c0:2d:75:e3:10:89:
|
||||
61:b1:6a:dd:f4:a4:c4:6a:d3:6f:27:30:7f:2d:07:78:d9:12:
|
||||
03:bc:a5:44:68:f3:10:bc:aa:32:e3:3f:6a:16:12:25:eb:82:
|
||||
ac:ae:30:ef:0d:be:87:11:13:e7:2f:78:69:67:36:62:ba:aa:
|
||||
51:8a:ee:6e:1e:ca:35:75:95:25:2d:db:e6:cb:71:70:95:25:
|
||||
76:99:13:02:57:99:56:25:a3:33:55:a2:6a:30:87:8b:97:e6:
|
||||
68:f3:c1:37:3c:c1:14:26:90:a0:dd:d3:02:3a:e9:c2:9e:59:
|
||||
d2:44
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDczCCAtygAwIBAgIBATANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER
|
||||
MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15
|
||||
IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy
|
||||
MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGkxCzAJBgNVBAYTAlVTMREwDwYDVQQI
|
||||
EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw
|
||||
DgYDVQQLEwdNeSBVbml0MRIwEAYDVQQDFAljbGllbnRfY24wgZ8wDQYJKoZIhvcN
|
||||
AQEBBQADgY0AMIGJAoGBAKUwsyvAvcspz+LY/WiwA8OmOxvsNnOhUl0n7gI1XFHt
|
||||
PTtU1xH1OJTu/cwMIqj4jhEvfENaqgc/lU9QIn2q4l0qkD0CGlvSzz/73Fgyxc4v
|
||||
gVgxIOs101PTQkfCE2iTYli2RmBIF9/SjMNAR89n6icPCXjp1SpkHsQzWtYNenmT
|
||||
AgMBAAGjggErMIIBJzAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM
|
||||
IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU5/sfRfBxd6+MEEoKQgP1
|
||||
Hx8Hz98wgZkGA1UdIwSBkTCBjoAUPfD3MD076zpVaPr1Q8nHrOE/EHiha6RpMGcx
|
||||
CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0
|
||||
eTEPMA0GA1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQDEwdS
|
||||
T09UIENBggkA6g5t/mS4aBkwCQYDVR0RBAIwADATBgNVHSUEDDAKBggrBgEFBQcD
|
||||
AjARBglghkgBhvhCAQEEBAMCB4AwDQYJKoZIhvcNAQELBQADgYEACLy0gKU7vpp4
|
||||
+Uc/wC114xCJYbFq3fSkxGrTbycwfy0HeNkSA7ylRGjzELyqMuM/ahYSJeuCrK4w
|
||||
7w2+hxET5y94aWc2YrqqUYrubh7KNXWVJS3b5stxcJUldpkTAleZViWjM1WiajCH
|
||||
i5fmaPPBNzzBFCaQoN3TAjrpwp5Z0kQ=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
clientDNSCert = `Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 4 (0x4)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA
|
||||
Validity
|
||||
Not Before: Dec 8 20:25:58 2014 GMT
|
||||
Not After : Dec 5 20:25:58 2024 GMT
|
||||
Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=client_dns
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:b0:6d:16:6a:fc:28:f7:dc:da:2c:a8:e4:0c:27:
|
||||
3c:27:ce:ae:d5:72:d9:3c:eb:af:3d:a3:83:98:5b:
|
||||
85:d8:68:f4:bd:53:57:d2:ad:e8:71:b1:18:8e:ae:
|
||||
37:8e:02:9c:b2:6c:92:09:cc:5e:e6:74:a1:4b:e1:
|
||||
50:41:08:9a:5e:d4:20:0b:6f:c7:c0:34:a8:e6:be:
|
||||
77:1d:43:1f:2c:df:dc:ca:9d:1a:0a:9f:a3:6e:0a:
|
||||
60:f1:6d:d9:7f:f0:f1:ea:66:9d:4c:f3:de:62:af:
|
||||
b1:92:70:f1:bb:8a:81:f4:9c:3c:b8:c9:e8:04:18:
|
||||
70:2f:77:74:48:d9:cd:e5:af
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
Netscape Comment:
|
||||
OpenSSL Generated Certificate
|
||||
X509v3 Subject Key Identifier:
|
||||
6E:A3:F6:01:52:79:4D:46:78:3C:D0:AB:4A:75:96:AC:7D:6C:08:BE
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78
|
||||
DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA
|
||||
serial:EA:0E:6D:FE:64:B8:68:19
|
||||
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:client_dns.example.com
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Client Authentication
|
||||
Netscape Cert Type:
|
||||
SSL Client
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
69:20:83:0f:16:f8:b6:f5:04:98:56:a4:b2:67:32:e0:82:80:
|
||||
da:8e:54:06:94:96:cd:56:eb:90:4c:f4:3c:50:80:6a:25:ac:
|
||||
3d:e2:81:05:e4:89:2b:55:63:9a:2d:4a:da:3b:c4:97:5e:1a:
|
||||
e9:6f:83:b8:05:4a:dc:bd:ab:b0:a0:75:d0:1e:b5:c5:8d:f3:
|
||||
f6:92:f1:52:d2:81:67:fc:6f:74:ee:49:37:73:08:bc:f5:26:
|
||||
86:67:f5:82:04:ff:db:5a:9f:f9:6b:df:2f:f5:75:61:f2:a5:
|
||||
91:0b:05:56:5b:e8:d1:36:d7:56:7a:ed:7d:e5:5f:2a:08:87:
|
||||
c2:48
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDjDCCAvWgAwIBAgIBBDANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER
|
||||
MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15
|
||||
IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy
|
||||
MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGoxCzAJBgNVBAYTAlVTMREwDwYDVQQI
|
||||
EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw
|
||||
DgYDVQQLEwdNeSBVbml0MRMwEQYDVQQDFApjbGllbnRfZG5zMIGfMA0GCSqGSIb3
|
||||
DQEBAQUAA4GNADCBiQKBgQCwbRZq/Cj33NosqOQMJzwnzq7Vctk86689o4OYW4XY
|
||||
aPS9U1fSrehxsRiOrjeOApyybJIJzF7mdKFL4VBBCJpe1CALb8fANKjmvncdQx8s
|
||||
39zKnRoKn6NuCmDxbdl/8PHqZp1M895ir7GScPG7ioH0nDy4yegEGHAvd3RI2c3l
|
||||
rwIDAQABo4IBQzCCAT8wCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNT
|
||||
TCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFG6j9gFSeU1GeDzQq0p1
|
||||
lqx9bAi+MIGZBgNVHSMEgZEwgY6AFD3w9zA9O+s6VWj69UPJx6zhPxB4oWukaTBn
|
||||
MQswCQYDVQQGEwJVUzERMA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENp
|
||||
dHkxDzANBgNVBAoTBk15IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMH
|
||||
Uk9PVCBDQYIJAOoObf5kuGgZMCEGA1UdEQQaMBiCFmNsaWVudF9kbnMuZXhhbXBs
|
||||
ZS5jb20wEwYDVR0lBAwwCgYIKwYBBQUHAwIwEQYJYIZIAYb4QgEBBAQDAgeAMA0G
|
||||
CSqGSIb3DQEBCwUAA4GBAGkggw8W+Lb1BJhWpLJnMuCCgNqOVAaUls1W65BM9DxQ
|
||||
gGolrD3igQXkiStVY5otSto7xJdeGulvg7gFSty9q7CgddAetcWN8/aS8VLSgWf8
|
||||
b3TuSTdzCLz1JoZn9YIE/9tan/lr3y/1dWHypZELBVZb6NE211Z67X3lXyoIh8JI
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
clientEmailCert = `Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 2 (0x2)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA
|
||||
Validity
|
||||
Not Before: Dec 8 20:25:58 2014 GMT
|
||||
Not After : Dec 5 20:25:58 2024 GMT
|
||||
Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=client_email
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:bf:f3:c3:d7:50:d5:64:d6:d2:e3:6c:bb:7e:5d:
|
||||
4b:41:63:76:9c:c4:c8:33:9a:37:ee:68:24:1e:26:
|
||||
cf:de:57:79:d6:dc:53:b6:da:12:c6:c0:95:7d:69:
|
||||
b8:af:1d:4e:8f:a5:83:8b:22:78:e3:94:cc:6e:fe:
|
||||
24:e2:05:91:ed:1c:01:b7:e1:53:91:aa:51:53:7a:
|
||||
55:6e:fe:0c:ef:c1:66:70:12:0c:85:94:95:c6:3e:
|
||||
f5:35:58:4d:3f:11:b1:5a:d6:ec:a1:f5:21:c1:e6:
|
||||
1f:c1:91:5b:67:89:25:2a:e3:86:27:6b:d8:31:7b:
|
||||
f1:0d:83:c7:f2:68:70:f0:23
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
Netscape Comment:
|
||||
OpenSSL Generated Certificate
|
||||
X509v3 Subject Key Identifier:
|
||||
76:22:99:CD:3D:BA:90:62:0F:BE:E7:5B:57:8D:31:1D:25:27:C6:6A
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78
|
||||
DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA
|
||||
serial:EA:0E:6D:FE:64:B8:68:19
|
||||
|
||||
X509v3 Subject Alternative Name:
|
||||
email:client_email@example.com
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Client Authentication
|
||||
Netscape Cert Type:
|
||||
SSL Client
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
80:70:19:d2:5c:c1:cf:d2:b6:e5:0e:76:cd:8f:c2:8d:a8:19:
|
||||
07:86:22:3f:a4:b1:98:c6:98:c1:dc:f8:99:5b:20:5c:6d:17:
|
||||
6b:fa:8b:4c:1b:86:14:b4:71:f7:41:22:03:ca:ec:2c:cd:ae:
|
||||
77:93:bd:08:06:8c:3c:06:ce:04:2c:b1:ce:79:20:0d:d5:01:
|
||||
1c:bd:66:60:38:db:4f:ad:dc:a6:33:8f:07:af:e6:bd:1c:27:
|
||||
4b:93:6a:4f:59:e3:cf:df:ff:87:f1:af:02:ad:50:06:f9:50:
|
||||
c7:59:87:bc:0c:e6:66:cd:d1:c8:df:e6:15:b2:21:b3:04:86:
|
||||
8c:89
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDkDCCAvmgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER
|
||||
MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15
|
||||
IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy
|
||||
MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGwxCzAJBgNVBAYTAlVTMREwDwYDVQQI
|
||||
EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw
|
||||
DgYDVQQLEwdNeSBVbml0MRUwEwYDVQQDFAxjbGllbnRfZW1haWwwgZ8wDQYJKoZI
|
||||
hvcNAQEBBQADgY0AMIGJAoGBAL/zw9dQ1WTW0uNsu35dS0FjdpzEyDOaN+5oJB4m
|
||||
z95XedbcU7baEsbAlX1puK8dTo+lg4sieOOUzG7+JOIFke0cAbfhU5GqUVN6VW7+
|
||||
DO/BZnASDIWUlcY+9TVYTT8RsVrW7KH1IcHmH8GRW2eJJSrjhidr2DF78Q2Dx/Jo
|
||||
cPAjAgMBAAGjggFFMIIBQTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu
|
||||
U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUdiKZzT26kGIPvudb
|
||||
V40xHSUnxmowgZkGA1UdIwSBkTCBjoAUPfD3MD076zpVaPr1Q8nHrOE/EHiha6Rp
|
||||
MGcxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkg
|
||||
Q2l0eTEPMA0GA1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQD
|
||||
EwdST09UIENBggkA6g5t/mS4aBkwIwYDVR0RBBwwGoEYY2xpZW50X2VtYWlsQGV4
|
||||
YW1wbGUuY29tMBMGA1UdJQQMMAoGCCsGAQUFBwMCMBEGCWCGSAGG+EIBAQQEAwIH
|
||||
gDANBgkqhkiG9w0BAQsFAAOBgQCAcBnSXMHP0rblDnbNj8KNqBkHhiI/pLGYxpjB
|
||||
3PiZWyBcbRdr+otMG4YUtHH3QSIDyuwsza53k70IBow8Bs4ELLHOeSAN1QEcvWZg
|
||||
ONtPrdymM48Hr+a9HCdLk2pPWePP3/+H8a8CrVAG+VDHWYe8DOZmzdHI3+YVsiGz
|
||||
BIaMiQ==
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
serverCert = `Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 7 (0x7)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA
|
||||
Validity
|
||||
Not Before: Dec 8 20:25:58 2014 GMT
|
||||
Not After : Dec 5 20:25:58 2024 GMT
|
||||
Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=127.0.0.1
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:e2:50:d9:1c:ff:03:34:0d:f8:b4:0c:08:70:fc:
|
||||
2a:27:2f:42:c9:4b:90:f2:a7:f2:7c:8c:ec:58:a5:
|
||||
0f:49:29:0c:77:b5:aa:0a:aa:b7:71:e7:2d:0e:fb:
|
||||
73:2c:88:de:70:69:df:d1:b0:7f:3b:2d:28:99:2d:
|
||||
f1:43:93:13:aa:c9:98:16:05:05:fb:80:64:7b:11:
|
||||
19:44:b7:5a:8c:83:20:6f:68:73:4f:ec:78:c2:73:
|
||||
de:96:68:30:ce:2a:04:03:22:80:21:26:cc:7e:d6:
|
||||
ec:b5:58:a7:41:bb:ae:fc:2c:29:6a:d1:3a:aa:b9:
|
||||
2f:88:f5:62:d8:8e:69:f4:19
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
Netscape Comment:
|
||||
OpenSSL Generated Certificate
|
||||
X509v3 Subject Key Identifier:
|
||||
36:A1:0C:B2:28:0C:77:6C:7F:96:90:11:CA:19:AF:67:1E:92:17:08
|
||||
X509v3 Authority Key Identifier:
|
||||
keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78
|
||||
DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA
|
||||
serial:EA:0E:6D:FE:64:B8:68:19
|
||||
|
||||
X509v3 Subject Alternative Name:
|
||||
<EMPTY>
|
||||
|
||||
X509v3 Extended Key Usage:
|
||||
TLS Web Server Authentication
|
||||
Netscape Cert Type:
|
||||
SSL Server
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
a9:dd:3d:64:e5:e2:fb:7e:2e:ce:52:7a:85:1d:62:0b:ec:ca:
|
||||
1d:78:51:d1:f7:13:36:1c:27:3f:69:59:27:5f:89:ac:41:5e:
|
||||
65:c6:ae:dc:18:60:18:85:5b:bb:9a:76:93:df:60:47:96:97:
|
||||
58:61:34:98:59:46:ea:d4:ad:01:6c:f7:4e:6c:9d:72:26:4d:
|
||||
76:21:1b:7a:a1:f0:e6:e6:88:61:68:f5:cc:2e:40:76:f1:57:
|
||||
04:5b:9e:d2:88:c8:ac:9e:49:b5:b4:d6:71:c1:fd:d8:b8:0f:
|
||||
c7:1a:9c:f3:3f:cc:11:60:ef:54:3a:3d:b8:8d:09:80:fe:be:
|
||||
f9:ef
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDczCCAtygAwIBAgIBBzANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER
|
||||
MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15
|
||||
IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy
|
||||
MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGkxCzAJBgNVBAYTAlVTMREwDwYDVQQI
|
||||
EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw
|
||||
DgYDVQQLEwdNeSBVbml0MRIwEAYDVQQDEwkxMjcuMC4wLjEwgZ8wDQYJKoZIhvcN
|
||||
AQEBBQADgY0AMIGJAoGBAOJQ2Rz/AzQN+LQMCHD8KicvQslLkPKn8nyM7FilD0kp
|
||||
DHe1qgqqt3HnLQ77cyyI3nBp39GwfzstKJkt8UOTE6rJmBYFBfuAZHsRGUS3WoyD
|
||||
IG9oc0/seMJz3pZoMM4qBAMigCEmzH7W7LVYp0G7rvwsKWrROqq5L4j1YtiOafQZ
|
||||
AgMBAAGjggErMIIBJzAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM
|
||||
IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUNqEMsigMd2x/lpARyhmv
|
||||
Zx6SFwgwgZkGA1UdIwSBkTCBjoAUPfD3MD076zpVaPr1Q8nHrOE/EHiha6RpMGcx
|
||||
CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0
|
||||
eTEPMA0GA1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQDEwdS
|
||||
T09UIENBggkA6g5t/mS4aBkwCQYDVR0RBAIwADATBgNVHSUEDDAKBggrBgEFBQcD
|
||||
ATARBglghkgBhvhCAQEEBAMCBkAwDQYJKoZIhvcNAQELBQADgYEAqd09ZOXi+34u
|
||||
zlJ6hR1iC+zKHXhR0fcTNhwnP2lZJ1+JrEFeZcau3BhgGIVbu5p2k99gR5aXWGE0
|
||||
mFlG6tStAWz3TmydciZNdiEbeqHw5uaIYWj1zC5AdvFXBFue0ojIrJ5JtbTWccH9
|
||||
2LgPxxqc8z/MEWDvVDo9uI0JgP6++e8=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
)
|
||||
|
||||
func TestX509(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Insecure bool
|
||||
Certs []*x509.Certificate
|
||||
|
||||
Opts x509.VerifyOptions
|
||||
User UserConversion
|
||||
|
||||
ExpectUserName string
|
||||
ExpectOK bool
|
||||
ExpectErr bool
|
||||
}{
|
||||
"non-tls": {
|
||||
Insecure: true,
|
||||
|
||||
ExpectOK: false,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"tls, no certs": {
|
||||
ExpectOK: false,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"self signed": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, selfSignedCert),
|
||||
User: CommonNameUserConversion,
|
||||
|
||||
ExpectErr: true,
|
||||
},
|
||||
|
||||
"server cert": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, serverCert),
|
||||
User: CommonNameUserConversion,
|
||||
|
||||
ExpectErr: true,
|
||||
},
|
||||
"server cert allowing non-client cert usages": {
|
||||
Opts: x509.VerifyOptions{Roots: getRootCertPool(t)},
|
||||
Certs: getCerts(t, serverCert),
|
||||
User: CommonNameUserConversion,
|
||||
|
||||
ExpectUserName: "127.0.0.1",
|
||||
ExpectOK: true,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"common name": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: CommonNameUserConversion,
|
||||
|
||||
ExpectUserName: "client_cn",
|
||||
ExpectOK: true,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"empty dns": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: DNSNameUserConversion,
|
||||
|
||||
ExpectOK: false,
|
||||
ExpectErr: false,
|
||||
},
|
||||
"dns": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientDNSCert),
|
||||
User: DNSNameUserConversion,
|
||||
|
||||
ExpectUserName: "client_dns.example.com",
|
||||
ExpectOK: true,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"empty email": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: EmailAddressUserConversion,
|
||||
|
||||
ExpectOK: false,
|
||||
ExpectErr: false,
|
||||
},
|
||||
"email": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientEmailCert),
|
||||
User: EmailAddressUserConversion,
|
||||
|
||||
ExpectUserName: "client_email@example.com",
|
||||
ExpectOK: true,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"custom conversion error": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) {
|
||||
return nil, false, errors.New("custom error")
|
||||
}),
|
||||
|
||||
ExpectOK: false,
|
||||
ExpectErr: true,
|
||||
},
|
||||
"custom conversion success": {
|
||||
Opts: getDefaultVerifyOptions(t),
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) {
|
||||
return &user.DefaultInfo{Name: "custom"}, true, nil
|
||||
}),
|
||||
|
||||
ExpectUserName: "custom",
|
||||
ExpectOK: true,
|
||||
ExpectErr: false,
|
||||
},
|
||||
|
||||
"future cert": {
|
||||
Opts: x509.VerifyOptions{
|
||||
CurrentTime: time.Now().Add(time.Duration(-100 * time.Hour * 24 * 365)),
|
||||
Roots: getRootCertPool(t),
|
||||
},
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: CommonNameUserConversion,
|
||||
|
||||
ExpectOK: false,
|
||||
ExpectErr: true,
|
||||
},
|
||||
"expired cert": {
|
||||
Opts: x509.VerifyOptions{
|
||||
CurrentTime: time.Now().Add(time.Duration(100 * time.Hour * 24 * 365)),
|
||||
Roots: getRootCertPool(t),
|
||||
},
|
||||
Certs: getCerts(t, clientCNCert),
|
||||
User: CommonNameUserConversion,
|
||||
|
||||
ExpectOK: false,
|
||||
ExpectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for k, testCase := range testCases {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
if !testCase.Insecure {
|
||||
req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs}
|
||||
}
|
||||
|
||||
a := New(testCase.Opts, testCase.User)
|
||||
|
||||
user, ok, err := a.AuthenticateRequest(req)
|
||||
|
||||
if testCase.ExpectErr && err == nil {
|
||||
t.Errorf("%s: Expected error, got none", k)
|
||||
continue
|
||||
}
|
||||
if !testCase.ExpectErr && err != nil {
|
||||
t.Errorf("%s: Got unexpected error: %v", k, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if testCase.ExpectOK != ok {
|
||||
t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectOK, ok)
|
||||
continue
|
||||
}
|
||||
|
||||
if testCase.ExpectOK {
|
||||
if testCase.ExpectUserName != user.GetName() {
|
||||
t.Errorf("%s: Expected user.name=%v, got %v", k, testCase.ExpectUserName, user.GetName())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultVerifyOptions(t *testing.T) x509.VerifyOptions {
|
||||
options := DefaultVerifyOptions()
|
||||
options.Roots = getRootCertPool(t)
|
||||
return options
|
||||
}
|
||||
|
||||
func getRootCertPool(t *testing.T) *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(getCert(t, rootCACert))
|
||||
return pool
|
||||
}
|
||||
|
||||
func getCert(t *testing.T, pemData string) *x509.Certificate {
|
||||
pemBlock, _ := pem.Decode([]byte(pemData))
|
||||
cert, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing cert: %v", err)
|
||||
return nil
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
func getCerts(t *testing.T, pemData ...string) []*x509.Certificate {
|
||||
certs := make([]*x509.Certificate, 0)
|
||||
for _, pemData := range pemData {
|
||||
certs = append(certs, getCert(t, pemData))
|
||||
}
|
||||
return certs
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// oidc implements the authenticator.Token interface using the OpenID Connect protocol.
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
"k8s.io/kubernetes/pkg/util"
|
||||
"k8s.io/kubernetes/pkg/util/net"
|
||||
)
|
||||
|
||||
var (
|
||||
maxRetries = 5
|
||||
retryBackoff = time.Second * 3
|
||||
)
|
||||
|
||||
type OIDCAuthenticator struct {
|
||||
clientConfig oidc.ClientConfig
|
||||
client *oidc.Client
|
||||
usernameClaim string
|
||||
groupsClaim string
|
||||
stopSyncProvider chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new OpenID Connect client with the given issuerURL and clientID.
|
||||
// NOTE(yifan): For now we assume the server provides the "jwks_uri" so we don't
|
||||
// need to manager the key sets by ourselves.
|
||||
func New(issuerURL, clientID, caFile, usernameClaim, groupsClaim string) (*OIDCAuthenticator, error) {
|
||||
var cfg oidc.ProviderConfig
|
||||
var err error
|
||||
var roots *x509.CertPool
|
||||
|
||||
url, err := url.Parse(issuerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if url.Scheme != "https" {
|
||||
return nil, fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", issuerURL, url.Scheme)
|
||||
}
|
||||
|
||||
if caFile != "" {
|
||||
roots, err = util.CertPoolFromFile(caFile)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to read the CA file: %v", err)
|
||||
}
|
||||
}
|
||||
if roots == nil {
|
||||
glog.Info("No x509 certificates provided, will use host's root CA set")
|
||||
}
|
||||
|
||||
// Copied from http.DefaultTransport.
|
||||
tr := net.SetTransportDefaults(&http.Transport{
|
||||
// According to golang's doc, if RootCAs is nil,
|
||||
// TLS uses the host's root CA set.
|
||||
TLSClientConfig: &tls.Config{RootCAs: roots},
|
||||
})
|
||||
|
||||
hc := &http.Client{}
|
||||
hc.Transport = tr
|
||||
|
||||
for i := 0; i <= maxRetries; i++ {
|
||||
if i == maxRetries {
|
||||
return nil, fmt.Errorf("failed to fetch provider config after %v retries", maxRetries)
|
||||
}
|
||||
|
||||
cfg, err = oidc.FetchProviderConfig(hc, strings.TrimSuffix(issuerURL, "/"))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
glog.Errorf("Failed to fetch provider config, trying again in %v: %v", retryBackoff, err)
|
||||
time.Sleep(retryBackoff)
|
||||
}
|
||||
|
||||
glog.Infof("Fetched provider config from %s: %#v", issuerURL, cfg)
|
||||
|
||||
if cfg.KeysEndpoint == "" {
|
||||
return nil, fmt.Errorf("OIDC provider must provide 'jwks_uri' for public key discovery")
|
||||
}
|
||||
|
||||
ccfg := oidc.ClientConfig{
|
||||
HTTPClient: hc,
|
||||
Credentials: oidc.ClientCredentials{ID: clientID},
|
||||
ProviderConfig: cfg,
|
||||
}
|
||||
|
||||
client, err := oidc.NewClient(ccfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// SyncProviderConfig will start a goroutine to periodically synchronize the provider config.
|
||||
// The synchronization interval is set by the expiration length of the config, and has a mininum
|
||||
// and maximum threshold.
|
||||
stop := client.SyncProviderConfig(issuerURL)
|
||||
|
||||
return &OIDCAuthenticator{ccfg, client, usernameClaim, groupsClaim, stop}, nil
|
||||
}
|
||||
|
||||
// AuthenticateToken decodes and verifies a JWT using the OIDC client, if the verification succeeds,
|
||||
// then it will extract the user info from the JWT claims.
|
||||
func (a *OIDCAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) {
|
||||
jwt, err := jose.ParseJWT(value)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if err := a.client.VerifyJWT(jwt); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
claim, ok, err := claims.StringClaim(a.usernameClaim)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("cannot find %q in JWT claims", a.usernameClaim)
|
||||
}
|
||||
|
||||
var username string
|
||||
switch a.usernameClaim {
|
||||
case "email":
|
||||
// TODO(yifan): Check 'email_verified' to make sure the email is valid.
|
||||
username = claim
|
||||
default:
|
||||
// For all other cases, use issuerURL + claim as the user name.
|
||||
username = fmt.Sprintf("%s#%s", a.clientConfig.ProviderConfig.Issuer, claim)
|
||||
}
|
||||
|
||||
// TODO(yifan): Add UID, also populate the issuer to upper layer.
|
||||
info := &user.DefaultInfo{Name: username}
|
||||
|
||||
if a.groupsClaim != "" {
|
||||
groups, found, err := claims.StringsClaim(a.groupsClaim)
|
||||
if err != nil {
|
||||
// Custom claim is present, but isn't an array of strings.
|
||||
return nil, false, fmt.Errorf("custom group claim contains invalid object: %v", err)
|
||||
}
|
||||
if found {
|
||||
info.Groups = groups
|
||||
}
|
||||
}
|
||||
return info, true, nil
|
||||
}
|
||||
|
||||
// Close closes the OIDC authenticator, this will close the provider sync goroutine.
|
||||
func (a *OIDCAuthenticator) Close() {
|
||||
// This assumes the s.stopSyncProvider is an unbuffered channel.
|
||||
// So instead of closing the channel, we send am empty struct here.
|
||||
// This guarantees that when this function returns, there is no flying requests,
|
||||
// because a send to an unbuffered channel happens after the receive from the channel.
|
||||
a.stopSyncProvider <- struct{}{}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
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 oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
"github.com/coreos/go-oidc/oidc"
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
type oidcProvider struct {
|
||||
mux *http.ServeMux
|
||||
pcfg oidc.ProviderConfig
|
||||
privKey *key.PrivateKey
|
||||
}
|
||||
|
||||
func newOIDCProvider(t *testing.T) *oidcProvider {
|
||||
privKey, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot create OIDC Provider: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
op := &oidcProvider{
|
||||
mux: http.NewServeMux(),
|
||||
privKey: privKey,
|
||||
}
|
||||
|
||||
op.mux.HandleFunc("/.well-known/openid-configuration", op.handleConfig)
|
||||
op.mux.HandleFunc("/keys", op.handleKeys)
|
||||
|
||||
return op
|
||||
|
||||
}
|
||||
|
||||
func (op *oidcProvider) handleConfig(w http.ResponseWriter, req *http.Request) {
|
||||
b, err := json.Marshal(op.pcfg)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (op *oidcProvider) handleKeys(w http.ResponseWriter, req *http.Request) {
|
||||
keys := struct {
|
||||
Keys []jose.JWK `json:"keys"`
|
||||
}{
|
||||
Keys: []jose.JWK{op.privKey.JWK()},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(time.Hour.Seconds())))
|
||||
w.Header().Set("Expires", time.Now().Add(time.Hour).Format(time.RFC1123))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func (op *oidcProvider) generateToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string, iat, exp time.Time) string {
|
||||
signer := op.privKey.Signer()
|
||||
claims := oidc.NewClaims(iss, sub, aud, iat, exp)
|
||||
claims.Add(usernameClaim, value)
|
||||
if groups != nil && groupsClaim != "" {
|
||||
claims.Add(groupsClaim, groups)
|
||||
}
|
||||
|
||||
jwt, err := jose.NewSignedJWT(claims, signer)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot generate token: %v", err)
|
||||
return ""
|
||||
}
|
||||
return jwt.Encode()
|
||||
}
|
||||
|
||||
func (op *oidcProvider) generateGoodToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
||||
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour))
|
||||
}
|
||||
|
||||
func (op *oidcProvider) generateMalformedToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
||||
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now(), time.Now().Add(time.Hour)) + "randombits"
|
||||
}
|
||||
|
||||
func (op *oidcProvider) generateExpiredToken(t *testing.T, iss, sub, aud string, usernameClaim, value, groupsClaim string, groups []string) string {
|
||||
return op.generateToken(t, iss, sub, aud, usernameClaim, value, groupsClaim, groups, time.Now().Add(-2*time.Hour), time.Now().Add(-1*time.Hour))
|
||||
}
|
||||
|
||||
// generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
|
||||
// This method is mostly identical to util.GenerateSelfSignedCert except for the 'IsCA' and 'KeyUsage'
|
||||
// in the certificate template. (Maybe we can merge these two methods).
|
||||
func generateSelfSignedCert(t *testing.T, host, certPath, keyPath string) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365),
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, host)
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate cert
|
||||
certBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate key
|
||||
keyBuffer := bytes.Buffer{}
|
||||
if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write cert
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(certPath, certBuffer.Bytes(), os.FileMode(0644)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write key
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(keyPath, keyBuffer.Bytes(), os.FileMode(0600)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCDiscoveryTimeout(t *testing.T) {
|
||||
maxRetries = 3
|
||||
retryBackoff = time.Second
|
||||
expectErr := fmt.Errorf("failed to fetch provider config after 3 retries")
|
||||
|
||||
_, err := New("https://foo/bar", "client-foo", "", "sub", "")
|
||||
if !reflect.DeepEqual(err, expectErr) {
|
||||
t.Errorf("Expecting %v, but got %v", expectErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCDiscoveryNoKeyEndpoint(t *testing.T) {
|
||||
var err error
|
||||
expectErr := fmt.Errorf("OIDC provider must provide 'jwks_uri' for public key discovery")
|
||||
|
||||
cert := path.Join(os.TempDir(), "oidc-cert")
|
||||
key := path.Join(os.TempDir(), "oidc-key")
|
||||
|
||||
defer os.Remove(cert)
|
||||
defer os.Remove(key)
|
||||
|
||||
generateSelfSignedCert(t, "127.0.0.1", cert, key)
|
||||
|
||||
op := newOIDCProvider(t)
|
||||
srv := httptest.NewUnstartedServer(op.mux)
|
||||
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
||||
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot load cert/key pair: %v", err)
|
||||
}
|
||||
srv.StartTLS()
|
||||
// TODO: Uncomment when fix #19254
|
||||
// defer srv.Close()
|
||||
|
||||
op.pcfg = oidc.ProviderConfig{
|
||||
Issuer: srv.URL,
|
||||
}
|
||||
|
||||
_, err = New(srv.URL, "client-foo", cert, "sub", "")
|
||||
if !reflect.DeepEqual(err, expectErr) {
|
||||
t.Errorf("Expecting %v, but got %v", expectErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCDiscoverySecureConnection(t *testing.T) {
|
||||
maxRetries = 3
|
||||
retryBackoff = time.Second
|
||||
|
||||
// Verify that plain HTTP issuer URL is forbidden.
|
||||
op := newOIDCProvider(t)
|
||||
srv := httptest.NewServer(op.mux)
|
||||
// TODO: Uncomment when fix #19254
|
||||
// defer srv.Close()
|
||||
|
||||
op.pcfg = oidc.ProviderConfig{
|
||||
Issuer: srv.URL,
|
||||
KeysEndpoint: srv.URL + "/keys",
|
||||
}
|
||||
|
||||
expectErr := fmt.Errorf("'oidc-issuer-url' (%q) has invalid scheme (%q), require 'https'", srv.URL, "http")
|
||||
|
||||
_, err := New(srv.URL, "client-foo", "", "sub", "")
|
||||
if !reflect.DeepEqual(err, expectErr) {
|
||||
t.Errorf("Expecting %v, but got %v", expectErr, err)
|
||||
}
|
||||
|
||||
// Verify the cert/key pair works.
|
||||
cert1 := path.Join(os.TempDir(), "oidc-cert-1")
|
||||
key1 := path.Join(os.TempDir(), "oidc-key-1")
|
||||
cert2 := path.Join(os.TempDir(), "oidc-cert-2")
|
||||
key2 := path.Join(os.TempDir(), "oidc-key-2")
|
||||
|
||||
defer os.Remove(cert1)
|
||||
defer os.Remove(key1)
|
||||
defer os.Remove(cert2)
|
||||
defer os.Remove(key2)
|
||||
|
||||
generateSelfSignedCert(t, "127.0.0.1", cert1, key1)
|
||||
generateSelfSignedCert(t, "127.0.0.1", cert2, key2)
|
||||
|
||||
// Create a TLS server using cert/key pair 1.
|
||||
tlsSrv := httptest.NewUnstartedServer(op.mux)
|
||||
tlsSrv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
||||
tlsSrv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert1, key1)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot load cert/key pair: %v", err)
|
||||
}
|
||||
tlsSrv.StartTLS()
|
||||
// TODO: Uncomment when fix #19254
|
||||
// defer tlsSrv.Close()
|
||||
|
||||
op.pcfg = oidc.ProviderConfig{
|
||||
Issuer: tlsSrv.URL,
|
||||
KeysEndpoint: tlsSrv.URL + "/keys",
|
||||
}
|
||||
|
||||
// Create a client using cert2, should fail.
|
||||
_, err = New(tlsSrv.URL, "client-foo", cert2, "sub", "")
|
||||
if err == nil {
|
||||
t.Fatalf("Expecting error, but got nothing")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestOIDCAuthentication(t *testing.T) {
|
||||
var err error
|
||||
|
||||
cert := path.Join(os.TempDir(), "oidc-cert")
|
||||
key := path.Join(os.TempDir(), "oidc-key")
|
||||
|
||||
defer os.Remove(cert)
|
||||
defer os.Remove(key)
|
||||
|
||||
generateSelfSignedCert(t, "127.0.0.1", cert, key)
|
||||
|
||||
// Create a TLS server and a client.
|
||||
op := newOIDCProvider(t)
|
||||
srv := httptest.NewUnstartedServer(op.mux)
|
||||
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
|
||||
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot load cert/key pair: %v", err)
|
||||
}
|
||||
srv.StartTLS()
|
||||
// TODO: Uncomment when fix #19254
|
||||
// defer srv.Close()
|
||||
|
||||
op.pcfg = oidc.ProviderConfig{
|
||||
Issuer: srv.URL,
|
||||
KeysEndpoint: srv.URL + "/keys",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
userClaim string
|
||||
groupsClaim string
|
||||
token string
|
||||
userInfo user.Info
|
||||
verified bool
|
||||
err string
|
||||
}{
|
||||
{
|
||||
"sub",
|
||||
"",
|
||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||
&user.DefaultInfo{Name: fmt.Sprintf("%s#%s", srv.URL, "user-foo")},
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
// Use user defined claim (email here).
|
||||
"email",
|
||||
"",
|
||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "", nil),
|
||||
&user.DefaultInfo{Name: "foo@example.com"},
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
// Use user defined claim (email here).
|
||||
"email",
|
||||
"",
|
||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
|
||||
&user.DefaultInfo{Name: "foo@example.com"},
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
// Use user defined claim (email here).
|
||||
"email",
|
||||
"groups",
|
||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-foo", "email", "foo@example.com", "groups", []string{"group1", "group2"}),
|
||||
&user.DefaultInfo{Name: "foo@example.com", Groups: []string{"group1", "group2"}},
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"sub",
|
||||
"",
|
||||
op.generateMalformedToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||
nil,
|
||||
false,
|
||||
"malformed JWS, unable to decode signature",
|
||||
},
|
||||
{
|
||||
// Invalid 'aud'.
|
||||
"sub",
|
||||
"",
|
||||
op.generateGoodToken(t, srv.URL, "client-foo", "client-bar", "sub", "user-foo", "", nil),
|
||||
nil,
|
||||
false,
|
||||
"oidc: JWT claims invalid: invalid claims, 'aud' claim and 'client_id' do not match",
|
||||
},
|
||||
{
|
||||
// Invalid issuer.
|
||||
"sub",
|
||||
"",
|
||||
op.generateGoodToken(t, "http://foo-bar.com", "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||
nil,
|
||||
false,
|
||||
"oidc: JWT claims invalid: invalid claim value: 'iss'.",
|
||||
},
|
||||
{
|
||||
"sub",
|
||||
"",
|
||||
op.generateExpiredToken(t, srv.URL, "client-foo", "client-foo", "sub", "user-foo", "", nil),
|
||||
nil,
|
||||
false,
|
||||
"oidc: JWT claims invalid: token is expired",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
client, err := New(srv.URL, "client-foo", cert, tt.userClaim, tt.groupsClaim)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
user, result, err := client.AuthenticateToken(tt.token)
|
||||
if tt.err != "" {
|
||||
if !strings.HasPrefix(err.Error(), tt.err) {
|
||||
t.Errorf("#%d: Expecting: %v..., but got: %v", i, tt.err, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("#%d: Unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(tt.verified, result) {
|
||||
t.Errorf("#%d: Expecting: %v, but got: %v", i, tt.verified, result)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.userInfo, user) {
|
||||
t.Errorf("#%d: Expecting: %v, but got: %v", i, tt.userInfo, user)
|
||||
}
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
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 tokenfile
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
type TokenAuthenticator struct {
|
||||
tokens map[string]*user.DefaultInfo
|
||||
}
|
||||
|
||||
// NewCSV returns a TokenAuthenticator, populated from a CSV file.
|
||||
// The CSV file must contain records in the format "token,username,useruid"
|
||||
func NewCSV(path string) (*TokenAuthenticator, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tokens := make(map[string]*user.DefaultInfo)
|
||||
reader := csv.NewReader(file)
|
||||
reader.FieldsPerRecord = -1
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(record) < 3 {
|
||||
return nil, fmt.Errorf("token file '%s' must have at least 3 columns (token, user name, user uid), found %d", path, len(record))
|
||||
}
|
||||
obj := &user.DefaultInfo{
|
||||
Name: record[1],
|
||||
UID: record[2],
|
||||
}
|
||||
tokens[record[0]] = obj
|
||||
|
||||
if len(record) >= 4 {
|
||||
obj.Groups = strings.Split(record[3], ",")
|
||||
}
|
||||
}
|
||||
|
||||
return &TokenAuthenticator{
|
||||
tokens: tokens,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *TokenAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) {
|
||||
user, ok := a.tokens[value]
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return user, true, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
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 tokenfile
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/auth/user"
|
||||
)
|
||||
|
||||
func TestTokenFile(t *testing.T) {
|
||||
auth, err := newWithContents(t, `
|
||||
token1,user1,uid1
|
||||
token2,user2,uid2
|
||||
token3,user3,uid3,"group1,group2"
|
||||
token4,user4,uid4,"group2"
|
||||
token5,user5,uid5,group5
|
||||
token6,user6,uid6,group5,otherdata
|
||||
token7,user7,uid7,"group1,group2",otherdata
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read tokenfile: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
Token string
|
||||
User *user.DefaultInfo
|
||||
Ok bool
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
Token: "token1",
|
||||
User: &user.DefaultInfo{Name: "user1", UID: "uid1"},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token2",
|
||||
User: &user.DefaultInfo{Name: "user2", UID: "uid2"},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token3",
|
||||
User: &user.DefaultInfo{Name: "user3", UID: "uid3", Groups: []string{"group1", "group2"}},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token4",
|
||||
User: &user.DefaultInfo{Name: "user4", UID: "uid4", Groups: []string{"group2"}},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token5",
|
||||
User: &user.DefaultInfo{Name: "user5", UID: "uid5", Groups: []string{"group5"}},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token6",
|
||||
User: &user.DefaultInfo{Name: "user6", UID: "uid6", Groups: []string{"group5"}},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token7",
|
||||
User: &user.DefaultInfo{Name: "user7", UID: "uid7", Groups: []string{"group1", "group2"}},
|
||||
Ok: true,
|
||||
},
|
||||
{
|
||||
Token: "token8",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
user, ok, err := auth.AuthenticateToken(testCase.Token)
|
||||
if testCase.User == nil {
|
||||
if user != nil {
|
||||
t.Errorf("%d: unexpected non-nil user %#v", i, user)
|
||||
}
|
||||
} else if !reflect.DeepEqual(testCase.User, user) {
|
||||
t.Errorf("%d: expected user %#v, got %#v", i, testCase.User, user)
|
||||
}
|
||||
|
||||
if testCase.Ok != ok {
|
||||
t.Errorf("%d: expected auth %v, got %v", i, testCase.Ok, ok)
|
||||
}
|
||||
switch {
|
||||
case err == nil && testCase.Err:
|
||||
t.Errorf("%d: unexpected nil error", i)
|
||||
case err != nil && !testCase.Err:
|
||||
t.Errorf("%d: unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadTokenFile(t *testing.T) {
|
||||
_, err := newWithContents(t, `
|
||||
token1,user1,uid1
|
||||
token2,user2,uid2
|
||||
token3,user3
|
||||
token4
|
||||
`)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsufficientColumnsTokenFile(t *testing.T) {
|
||||
_, err := newWithContents(t, "token4\n")
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non error")
|
||||
}
|
||||
}
|
||||
|
||||
func newWithContents(t *testing.T, contents string) (auth *TokenAuthenticator, err error) {
|
||||
f, err := ioutil.TempFile("", "tokenfile_test")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating tokenfile: %v", err)
|
||||
}
|
||||
f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil {
|
||||
t.Fatalf("unexpected error writing tokenfile: %v", err)
|
||||
}
|
||||
|
||||
return NewCSV(f.Name())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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 tokentest
|
||||
|
||||
import "k8s.io/kubernetes/pkg/auth/user"
|
||||
|
||||
type TokenAuthenticator struct {
|
||||
Tokens map[string]*user.DefaultInfo
|
||||
}
|
||||
|
||||
func New() *TokenAuthenticator {
|
||||
return &TokenAuthenticator{
|
||||
Tokens: make(map[string]*user.DefaultInfo),
|
||||
}
|
||||
}
|
||||
func (a *TokenAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) {
|
||||
user, ok := a.Tokens[value]
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
return user, true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user