Squashed 'k8smain/' content from commit 719158d

git-subtree-dir: k8smain
git-subtree-split: 719158d2c8788c30cc9e1c51bbebb53c5801524e
This commit is contained in:
johndmulhausen
2016-03-01 18:18:53 -08:00
commit 6bd2f3f318
8011 changed files with 1899339 additions and 0 deletions
+18
View File
@@ -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
}
+18
View File
@@ -0,0 +1,18 @@
/*
Copyright 2016 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 authorizer contains implementations for pkg/auth/authorizer interfaces
package authorizer
@@ -0,0 +1,211 @@
/*
Copyright 2016 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.
*/
// This file was generated using openssl by the gencerts.sh script
// and holds raw certificates for the webhook tests.
package webhook
var caKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA6IVXGPX5yP2Q6TAlQXIQsavzSqZ973iZvpQBGTI6M98gTSVm
eBYE3o7S8e6WTI3DCnWwqc8Md1rT92FtaQLwv+uMNXijLio5RVBqjUEbunD5In/+
T/y5sE9P3CzcWy6CEhIvORAZj6UlvgZzbRwI91+EVFR5jd8JU0e/L9Ds1jLZFyQw
Kc1ADo+Tj9O4l0WtpRlrhzTgoor4C3fAQZm0mq+llTnxCmw+lhy8t88bPG1cMwdd
DtUTbpetc++2JZ62Q3F1nqcX1EcHDidR0x3j+3357BLkXRK4MQsWLYLzeZ3X1ghW
XT062H866PcIV+MX4H58spMN5cVYk5YTneGihQIDAQABAoIBAHU7FQieq4ssXK1U
+tOeQNBzUzxl6MSd11YApPUhH7sbWdvLaXhOEbJr6+rSUbDTIGzbnXBf1XcvsgLd
eh4hv2PjzFMBObSC0VEjFDWXh/VeFB3SzlNhpfVAZ5EohQjrz+RwiqKIfXqw1vCR
rAxswBCIdd1WodpngvocCEaBXYc4MblaPhJDVtxQe8ndEakkSDlX9Z3qIaIGyXRa
NvY/yURVuXhwDDd7C2QBT6CXGWhldAg7xrRVTcIoqAUfZCgfis0H8cQOa1cGNsbW
t/oHm1fYTxMKFPhWQG0oimx+XJ07BeGgraDRLnxxNnGWTg/W33bc0ZCxCVT0Q5p9
kMMfQUECgYEA9cewTK4ZRKC4bTdwqLTh3cyMkbyN4kBHmB1mS2FV/T0l4oZThM//
OZ6KFnRCuvfuJIOa70s2bqUYky8NTQAidnnbTW2nZ/E5JdeIBs1fAfadAqiPdmkf
MhvjBF/XfLnbCuXx3jA7GmNCpunJysuLtQzwlQlZLojN231uS+3LFbkCgYEA8jCC
MgKYaDWssQbT7zfk5MxyZIH3F9N8K2RBIDSVuMo/E1LCIJ06/k+4jdv8nAWYJXcN
eyLG7l0SXqrpMBSc9+ZTJgmbo0Mw+npvJHbJvAtD/XOSPjlIqkzPAUrxuiBYxa5S
IfKZibygXKAbQMEwY7I4sTbBtIyiQmo9csxt2S0CgYEAiBi1VSCquUfOGBw09BaF
Y85aoHCqmHhDrMXK2T7i4MG1csQzBz4t8/gIOvrR4LpdUjbV2l/pmkctXoMVeGf0
rWo4t51ar8HxhTTeC/Y4/9tRgiFYn5cCQTsT8F4p8tTvqA9AaWqHr8r7I3Yd2X/w
sqahqcVtbskuRLYmF0FrzXECgYAeiR0xPwCGSxYt78Vy6OI0Ms7Ne1FzMJf8RJSt
gdPKy70uK4YMZKaWf+iuAimUZmQrfRo3B0h7r0JsqzHhfQfZfbHIHvf/mq4nNp6i
w1NmISl+YD71F3Xg+vQynodhx0hKDFOQsizHn/+8DffBr1nxh/v75AKCSCUBKLH8
sme7NQKBgDHQac2TmDSelE2uXTGxEVDQs/EpdJh7oCTLQ99Xud/DsaCOrt2s7aRX
1FEohsCaUnqwS07/iH2o6Qb/qOteufB9I7FG85nAvqmP5dI4crGNNa8Rl6fXJaR8
TUwpZmylTKEJ9zLt2PADglyDrQ2D+1WNzh966Oo9c+kZt4WJM0aF
-----END RSA PRIVATE KEY-----`)
var caCert = []byte(`-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIJAKK9m2Cfg5uhMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
MzQ0MjhaMBsxGTAXBgNVBAMMEHdlYmhvb2tfYXV0aHpfY2EwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQDohVcY9fnI/ZDpMCVBchCxq/NKpn3veJm+lAEZ
Mjoz3yBNJWZ4FgTejtLx7pZMjcMKdbCpzwx3WtP3YW1pAvC/64w1eKMuKjlFUGqN
QRu6cPkif/5P/LmwT0/cLNxbLoISEi85EBmPpSW+BnNtHAj3X4RUVHmN3wlTR78v
0OzWMtkXJDApzUAOj5OP07iXRa2lGWuHNOCiivgLd8BBmbSar6WVOfEKbD6WHLy3
zxs8bVwzB10O1RNul61z77YlnrZDcXWepxfURwcOJ1HTHeP7ffnsEuRdErgxCxYt
gvN5ndfWCFZdPTrYfzro9whX4xfgfnyykw3lxViTlhOd4aKFAgMBAAGjUDBOMB0G
A1UdDgQWBBSumZL6MMwmFGyhQAwl/v0lYDzdZjAfBgNVHSMEGDAWgBSumZL6MMwm
FGyhQAwl/v0lYDzdZjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAG
6k+bZxKYq4PVZHWTKA7RSjv95FMMr4RSFwKn/n8TUD44ANWYqDrEfVmxAMn3NVK9
ckA8mIRym4IGiWD9eBGgPNNtbAq8Wl/9+5qbDMerpXuRnG3wNY7RU75Rl008m52r
c2i86ZPUi2fAJZyMf5StWE21oKiDYYQqlB6xxsIj6OHhf7536vEysoztNX5FpS2n
q8wG0EhJVhG+Qyww8IlZA5Cjoh71Eqkcwb4cuLjPypxmLm0ywZ/6KgzV+IF+CT2v
TJIpMokDUKlRi9cWSqkWXFE6xbCmhrrwKYsi0X6Vvi7a0pmOnSzKCQl8jN8u4A9R
xar2YeJ6mCCzSAPM69DP
-----END CERTIFICATE-----`)
var badCAKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAon7dRV4Br10dLcf8zgs/hOHouELveFr8tuWVIFivxSdnac2k
6dM4iQ2uYS9nTXxNhyJJ/TX/MHEYc4gSXoqUbtx9jE3VA4mCKDhO7cJtCYxq0QV/
PlQCiAPjn5nUMt9ACdii7/uTFDl46bK9K6ajvKHfHoWeYaJsF54kxBq5IMj+QaB2
nc+pba00bGG09sYcHyD37QH+ugx64x+21xMYj2LB/uPoqZM0kj1GHPxAs8GqFq2P
gwkv589AlHqt2iMCTAqED2jcg4FeS2r1DeYHwGyGAPfWTdA8RZ+gZ/P0Gj91T+4B
9srR7BybUFjf1KxEcvPXBvP5r8OwOiYjS8hx/wIDAQABAoIBAQCVBQ9bfDjDX/tQ
buVS+FHKRXss8IW4tIiqGqXGQk7/2YEnMKaaoVBpsBhJnDV6hBJ9aV69TnW3MSCh
YxqlhSVW/fJNZ1uAoOyygeEwfmuMpC+ZfRcSS+z+W8K2LVbDSKXr4babqvVZSNOw
TnDZxTrH1RNPZG65T0Ed77P7/B3nB7aeB2UMuHMQNZ3KrYDTck2R2uTGp+29TplN
blS4VAg2/9KqFr7jkS3/C4jjxVd7d9mm0VdAvLcvENVXqSTYV8xDp+VLTnmtXi5f
LXcopS+zKtKqT7MM7RA2sKrmSfrQBIXW2E1kfDFtpZHajhDutdYkSTH665W1G23M
dIgy3ajhAoGBANE4AhMUVfQqXUCU0UjUDxiOy/8XcKiW/dKhRR1DOQY24J/k+UWv
PEGVcBW4tgalYkTl/AW6hsNfubZaJuw05cHIKdL3df6ug7BUiJpmIv3sjrvPRYvA
WY1UTb3EJrswGz8S2l5+2S3WFTCfK7S6N6Stfi1x6rMJBuOss7HGqdh3AoGBAMbU
WavRqGRsvJFfE5bahXbFpkGWT++BTMP+lzK31z24JjmJdwO+ABWU4/xaXayA4skH
PrzlYUcGJWIedb6W4dvz0sA59yflQzYmREkQPE+wbyor003y7mB8LpFiCnfaFhRn
hoowkyIY+xM4UeDXWWt3DhBElgfA8fYZdiNJEhy5AoGBAMwYUw3BvMffu/CQPElL
dR6DzsUeXKxZ/2pGIGIXfb1uM1pHyFQOSj3ARgMqmYeKNn73zA7akzRsYYJeF7I9
OBT96q7+8IBuRdDx5gCYunHzHppf7HwUPEf+gYgpnY7lsu6ouZWNMNfiC/HOlJhN
QJLJHFnA0y+sEqhvhSxbnLypAoGBALHCZ+kVKFegX3YYaosUEv589obsu8qE7vzL
QKI3elfTq1kFbUILPEgPNUUIBXeUQy03LP/0k2PMOt/eG6apfoQHGQSCzlT8w3pF
/AbWXRVhyAEL7X5jEntwirGv1WwRrmvPopkplGGHs/EbCRjbbzaE2i3xI7EK70f2
u4gQbAEBAoGAVR4u8g5Tx2Gunzh7tfJJ5e3xGBGS3Yq+JqUVNI6t6KIAPh0rM+aD
9tDgcwn8Vn5YU7YkqA2T8OOFsbJfrfZ7y7+oeMFukuIyxgmy9n/V/tCIrV/lR7A5
3iYhanTUbQswx19pSRgsXi7fo9Fi/dmUwyHi18uz5FdLyCTsMbf3uA8=
-----END RSA PRIVATE KEY-----`)
var badCACert = []byte(`-----BEGIN CERTIFICATE-----
MIIDCzCCAfOgAwIBAgIJAPqJyUfmRxGLMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
MzQ0MjhaMBsxGTAXBgNVBAMMEHdlYmhvb2tfYXV0aHpfY2EwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQCift1FXgGvXR0tx/zOCz+E4ei4Qu94Wvy25ZUg
WK/FJ2dpzaTp0ziJDa5hL2dNfE2HIkn9Nf8wcRhziBJeipRu3H2MTdUDiYIoOE7t
wm0JjGrRBX8+VAKIA+OfmdQy30AJ2KLv+5MUOXjpsr0rpqO8od8ehZ5homwXniTE
GrkgyP5BoHadz6ltrTRsYbT2xhwfIPftAf66DHrjH7bXExiPYsH+4+ipkzSSPUYc
/ECzwaoWrY+DCS/nz0CUeq3aIwJMCoQPaNyDgV5LavUN5gfAbIYA99ZN0DxFn6Bn
8/QaP3VP7gH2ytHsHJtQWN/UrERy89cG8/mvw7A6JiNLyHH/AgMBAAGjUDBOMB0G
A1UdDgQWBBS6IGeGHZCylibt0GzY0dP6C0J9VjAfBgNVHSMEGDAWgBS6IGeGHZCy
libt0GzY0dP6C0J9VjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAi
A1dp75kbePFZsUNjxN6B/Pv0vSoaOjQkc4hpxKbI4VRCuPGmMRFYTlKCzoZ53OqQ
2Jmu1Zbzel/bV5vXrW0BOfUpfWYzd/usIJEuTgU8ijBIB+IHAXYwwxeKRcz3C+7+
9RBMF7gSg9pU2hrSvjhh7Q96IMJ42Z7tI3WD8SZaQLjY1NW1jrQVsg66ktdMke7x
zC8oIRIBH4W6l5s7jtZx1k305NE04pigcFLxCxOmicKd66ysI5hAZkD7y0dgwgtL
IqCQy6t7uJDydRiNRfPFr9Eg7uOu83JGw11f3bGVhJVCbzHyKddvkQsQbdaMHRgZ
zgmWLORg+ls1H1oaJiNW
-----END CERTIFICATE-----`)
var serverKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtegsP499au5ZxlwM26rk3TnRgakchQi/9bhfMr0LaEKng1lR
XopzzGuGeZQswzbx7iiH89JzFkurZoEmZwtS4Aybit92VOSv0EUnyx7WR3V21ObZ
iQO0rr0UmG84NjdzATkqF+R5Z+HN9shwgBI4PR1j/ybCt7jNz+OM/VmqsgzoKLoa
bGrx7LCTPk8y5G8AoPOrIAP+9WHJsKQSRT8Lru4lYqseBxvhjqo8NRqzZLg79ldY
aKFqa2N2zr5qp94sG3/zihNDxjZvyyn9c8qvPBL0xOyayvOJG8eZUmjQpUMv7Jk3
qFmdMgGaDJRw0Qg6+/Zt6MHNs6Rbb8hmwuMSpwIDAQABAoIBAQCjzeFijwzKKL4w
0B1IBhi3WeReFPG4nkt1ssQPBYrrJPKBZgHO13A1STI78wFn/OdYpajfF8hI8HT1
BiGVsu27Eb9TC60b/x6OtmeCEk+044LRbtu+9NZUb7HHHogI0l++X0KXZ0coE38L
1izwNvfrmLa+QaIgHMtAg9EnJwJ993n4L31GovWh8MGmVyJX/F92y+agNwWkNYYp
iLWFyon+HbNVL13WOOYnYEdA8Me3+Gucy1EOfWMF7mgmuO2vcfnxXd6b16VjAwtE
jGCQfzgpWGHLpgwoBgDmnPUbdNPUT3MbA9jqG2mlnBSBQveYgKrmFdDYnAjnCM4L
uF2ztBzhAoGBAOYc3sF3YjpIIMsyH9omqtfOuxO+oZkpb2vB9kgdXCDcG870M+BC
bNzV7DCSV8QAUqjKQK1r3gq62UZMLXZbG8x5UnM8/EK0X1CSqygwSWjGpYxIQEhh
O2lq69WipkNDnX1ZmrvEdHD2cxqkkXZ7bdRKRasrFJgvJa3XbiJ18KYxAoGBAMpe
/72EcX9oL3KT8tJSpvasrw17p/XkMMCxTp3IDb3krF/4k5bYF61F68/LNSy3xkos
ZrPUK/U160iuHSYCpMq4pPmlWgKq4hmUMOt+8Yy622zDlugarq9VLqvSdGHm+r6F
5fHilXB0UsTXXOuLZWLcSQ0MBgiaVCLb2AmXZhhXAoGAEjSchw/r7JKCTbE0hezj
PVm0wVYmsNhvYUYiNwhjnpHrfU8iv45h0IL4QcuCOBaSc5o0zcOn+I9Z207xldiV
dXLvzAA6MQjWNai08+QGGs0EkfmxZEiVC70S1X8dylqSHjW1oT9kuv80khoNDCOt
x8rsgiNRaMzqHTvbEczk8jECgYB2Od+wSULBSw2FI5fVdcHjFGlEODycs44j1LH4
DZqxmHl3q9IVavMSIGouQCo1kLuAM8ZgQpDXtYNaN5YB0cOSRyLiUc5vBoQGq4OU
4Nme/L8aIH315TiuZ9ZXPSEO3REZ40G9+UCSrPJ52tOHLC2z/ruSqraPqhGDN+pT
WCamCwKBgEPa+kVrPs0khQH8+sbFbU9ifj4fhPAiSwj2fKuXFro2mE205vAMHye/
SYs/mPzYzKSd7F+7Zk6oVrgFVskTiReW3phF+cIl+CdcnIenF0jW1PVgGw8znu+P
SbHSdqV+tB7AW2J7sH8TZtfMUPAK2MJ4S+1uaHK86K79ym4Rz0E2
-----END RSA PRIVATE KEY-----`)
var serverCert = []byte(`-----BEGIN CERTIFICATE-----
MIIC/zCCAeegAwIBAgIJAN7rkfhaX8FZMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
MzQ0MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfYXV0aHpfc2VydmVyMIIBIjANBgkq
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtegsP499au5ZxlwM26rk3TnRgakchQi/
9bhfMr0LaEKng1lRXopzzGuGeZQswzbx7iiH89JzFkurZoEmZwtS4Aybit92VOSv
0EUnyx7WR3V21ObZiQO0rr0UmG84NjdzATkqF+R5Z+HN9shwgBI4PR1j/ybCt7jN
z+OM/VmqsgzoKLoabGrx7LCTPk8y5G8AoPOrIAP+9WHJsKQSRT8Lru4lYqseBxvh
jqo8NRqzZLg79ldYaKFqa2N2zr5qp94sG3/zihNDxjZvyyn9c8qvPBL0xOyayvOJ
G8eZUmjQpUMv7Jk3qFmdMgGaDJRw0Qg6+/Zt6MHNs6Rbb8hmwuMSpwIDAQABo0Aw
PjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggrBgEFBQcDATAP
BgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQCZHB9UCl2CfylWP3db
xUamawnRoTYlsOcUh4f2tlHMY+vYiEStN+LECk62YpeaHl/nz/lk7g1Jx9aua39z
wFIHiXYhwSWOtgmzpbxYLye1yajKXbbA1T7mEZJTjewDB9i1LcB9W3EV5VJ8Y1GY
AYKuKQ4Cb1HrqLsrw/1PDm0VouWzf2ESv8CBvAv/pYLVfwgS6WsUqn9wycpLEnqQ
RK66/AoiOaxUIjEP0O1q6pi6Mag7XAfeNtx8J0VGt4cRG4rvWCbKVUyvKfUCkipN
gJu09S+KIz3x1CJLRuJX9tB+cFnnykDLQ2IKg7x44O83ikNk8+Di3iT/awCguWPE
rHh5
-----END CERTIFICATE-----`)
var clientKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA5ij4WXWGvbmfAYhEafKRvLEHSkUCYIDjwQAlnHoLf/lz+Fh2
DEv4lcBaycwk3+LVUGKgYOg91txYJvGD3HcmVThXZvcgJd4V9Ll3aY/6xVRCenWi
UNgVQVQITGkMn09ZkSXbZCK4wqz9oTVh0Ti5a7apOS2V07yL0q7vw003v5TBqzC/
FgRwE0bv1rKYYQ80WbDlYkkYGf216zQTwS4g/nShCZAX9eqSfbBg6B/A3OwpbIfx
09BWuwWhp5QnS4w002gGWavRFNzu8pUHUv6zMN8OKpasv+Na+ZB+gMt4+e2Y7qNz
76QL23eGwc6oWn8lQBtkDLmLIa6jbWX067U76QIDAQABAoIBAQCJpGzJSzC2W8DM
sMqBNdCUMKZ0cwq13b7W2BimGJKyCOOi3HxUZEaYf/2Leyt+PPBm72SML7dzvDh3
qa269gKVqmkSqa2vF763qQbRuYo14msTQzA7+s3TUMbZs2UaDOE6nZIzs1QdEElp
1DvYXHz+/rD7Adj9VF+mMnouqQoy5kgJTnVZ8sOyl/9R6F67xKBIvcrtPfqVZzuG
2hGAMUnawxFUajQC7BynIeCWrk79SUmQgilyNgRdY6+rGh2uRupIxuiAukPtuag1
Li+wnNl1UGECtv9ZnnboKvg2334k5vhYScGRJbwbr7Zt3ZaNd0Z/DE9kTtnhBS7v
9qWdc7CBAoGBAPR4hz1fhHFiPmMEAGuiNms6WdyIfyonIRYas8ZDKUQGdxn/aO8a
CURktHRlm6iYT+j1cbf3RnLEN9pNr3V2EySOMc+rXUNifcP7Vl53akAQmISUfQWG
UfwaNLicbavf6m9UCiwWByAZghqDZSLiwmLHIjGcSJQiFuhZryioDydxAoGBAPED
q1Z7oNhzwRYie9OB5ylnrCH8G3yFl8egBmQrPJKIQHA9mAGg01LEJwQNoWewyAWx
jfeFtWvIgZkj49cluZgHYyF81jApaNraxtXAgIwC1n7oAIttmeklZ/V1HntknG3Y
ow2bV/NA3aPOTPYxW8oDv7U9lvwve7kIFxeWjE/5AoGASfXI3G1wUSkqvKPySJ3b
ntcZZpm49xS9csWDS+D3tAfMsoXNxkB3O0TIP0qaLAhgbJcM314k5wWr7BSCl6Ow
KOgH887hOUirycXZHF0+PMGIktulcy1u0jlPZ+aTW2MztpiTN0E2yKRO8xx7VXGK
431hP+cLIh2qFoNDdaZaZ1ECgYEArw++PWQxMefqgVxs2vXJZY7TPiA0Ct+ynqKC
4fFx3vGu9JgYuF4MAVtPB6eq7HlA4LnWZ8ssOuz6DbU/AoB5bY84FxPpNDRv4D/3
Gz3nYUuSZ72234+tsuaju2vlxzUOVs97qB+E48Di/N+VkWHKzVKpxkjFScpnsL/K
niyRIGkCgYEAriuxbOCczL/j6u2Xq1ngEsGg+RXjtOYGoJWo7B8qlVL4nF8w1Nbd
FxEmOChQgUnBdwb93qHCSq0Fidf7OfewrfJJkstWIh3zPS4umLZo7R3YblncpdfT
M197uckIWccZml2jF/c7nvK+MjwDRhkOl2a6HzMxcdBwYUJmSwmIZ4k=
-----END RSA PRIVATE KEY-----`)
var clientCert = []byte(`-----BEGIN CERTIFICATE-----
MIIC7jCCAdagAwIBAgIJAN7rkfhaX8FaMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
BAMMEHdlYmhvb2tfYXV0aHpfY2EwIBcNMTYwMjE2MjM0NDI4WhgPMjI4OTEyMDEy
MzQ0MjhaMB8xHTAbBgNVBAMMFHdlYmhvb2tfYXV0aHpfY2xpZW50MIIBIjANBgkq
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5ij4WXWGvbmfAYhEafKRvLEHSkUCYIDj
wQAlnHoLf/lz+Fh2DEv4lcBaycwk3+LVUGKgYOg91txYJvGD3HcmVThXZvcgJd4V
9Ll3aY/6xVRCenWiUNgVQVQITGkMn09ZkSXbZCK4wqz9oTVh0Ti5a7apOS2V07yL
0q7vw003v5TBqzC/FgRwE0bv1rKYYQ80WbDlYkkYGf216zQTwS4g/nShCZAX9eqS
fbBg6B/A3OwpbIfx09BWuwWhp5QnS4w002gGWavRFNzu8pUHUv6zMN8OKpasv+Na
+ZB+gMt4+e2Y7qNz76QL23eGwc6oWn8lQBtkDLmLIa6jbWX067U76QIDAQABoy8w
LTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggrBgEFBQcDAjAN
BgkqhkiG9w0BAQsFAAOCAQEA2IZNhkVrSTAIeP2N2WzOHqbFbGyO+NA8G9Hb5fiX
e1YS2Ku3ERYNr+HLxNHCsXiSUKjjBmXMc4z0XaHJznEKEbotZftjTlTQlHi3/5vm
dIG18pmO/E5ebVXl6pU96v/hBd8N5rWp9WUKgP0y59r/JA+oNpmd10A+RyaOyrFK
rBm8Z8rvDYMrXSpOwx9BNDuhqzbdG8MYw5vO55Er3hwTXoapsMqSh5s9+OFFpUJi
2uEoQlwWiYRtQj6g4wgr4woDEbv8XxsHqGfs+GSnmRsB69xRI24lEtC+nS6Rz3Sh
YWeN0gD8PsQC1KJVv6xCGo1yXSEwytRMB23XYtAZahLdLg==
-----END CERTIFICATE-----`)
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
# Copyright 2016 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.
set -e
# gencerts.sh generates the certificates for the webhook authz plugin tests.
#
# It is not expected to be run often (there is no go generate rule), and mainly
# exists for documentation purposes.
cat > server.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
EOF
cat > client.conf << EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
# Create a certificate authority
openssl genrsa -out caKey.pem 2048
openssl req -x509 -new -nodes -key caKey.pem -days 100000 -out caCert.pem -subj "/CN=webhook_authz_ca"
# Create a second certificate authority
openssl genrsa -out badCAKey.pem 2048
openssl req -x509 -new -nodes -key badCAKey.pem -days 100000 -out badCACert.pem -subj "/CN=webhook_authz_ca"
# Create a server certiticate
openssl genrsa -out serverKey.pem 2048
openssl req -new -key serverKey.pem -out server.csr -subj "/CN=webhook_authz_server" -config server.conf
openssl x509 -req -in server.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out serverCert.pem -days 100000 -extensions v3_req -extfile server.conf
# Create a client certiticate
openssl genrsa -out clientKey.pem 2048
openssl req -new -key clientKey.pem -out client.csr -subj "/CN=webhook_authz_client" -config client.conf
openssl x509 -req -in client.csr -CA caCert.pem -CAkey caKey.pem -CAcreateserial -out clientCert.pem -days 100000 -extensions v3_req -extfile client.conf
outfile=certs_test.go
cat > $outfile << EOF
/*
Copyright 2016 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.
*/
EOF
echo "// This file was generated using openssl by the gencerts.sh script" >> $outfile
echo "// and holds raw certificates for the webhook tests." >> $outfile
echo "" >> $outfile
echo "package webhook" >> $outfile
for file in caKey caCert badCAKey badCACert serverKey serverCert clientKey clientCert; do
data=$(cat ${file}.pem)
echo "" >> $outfile
echo "var $file = []byte(\`$data\`)" >> $outfile
done
# Clean up after we're done.
rm *.pem
rm *.csr
rm *.srl
rm *.conf
@@ -0,0 +1,180 @@
/*
Copyright 2016 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 webhook implements the authorizer.Authorizer interface using HTTP webhooks.
package webhook
import (
"errors"
"fmt"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apimachinery/registered"
"k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/runtime/serializer/json"
"k8s.io/kubernetes/pkg/runtime/serializer/versioning"
_ "k8s.io/kubernetes/pkg/apis/authorization/install"
)
var (
encodeVersions = []unversioned.GroupVersion{v1beta1.SchemeGroupVersion}
decodeVersions = []unversioned.GroupVersion{v1beta1.SchemeGroupVersion}
requireEnabled = []unversioned.GroupVersion{v1beta1.SchemeGroupVersion}
)
// Ensure Webhook implements the authorizer.Authorizer interface.
var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
type WebhookAuthorizer struct {
restClient *restclient.RESTClient
}
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
//
// The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
//
// # clusters refers to the remote service.
// clusters:
// - name: name-of-remote-authz-service
// cluster:
// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
// server: https://authz.example.com/authorize # URL of remote service to query. Must use 'https'.
//
// # users refers to the API server's webhook configuration.
// users:
// - name: name-of-api-server
// user:
// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
// client-key: /path/to/key.pem # key matching the cert
//
// For additional HTTP configuration, refer to the kubeconfig documentation
// http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
func New(kubeConfigFile string) (*WebhookAuthorizer, error) {
for _, groupVersion := range requireEnabled {
if !registered.IsEnabledVersion(groupVersion) {
return nil, fmt.Errorf("webhook authz plugin requires enabling extension resource: %s", groupVersion)
}
}
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeConfigFile
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err := loader.ClientConfig()
if err != nil {
return nil, err
}
serializer := json.NewSerializer(json.DefaultMetaFactory, api.Scheme, runtime.ObjectTyperToTyper(api.Scheme), false)
clientConfig.ContentConfig.Codec = versioning.NewCodecForScheme(api.Scheme, serializer, encodeVersions, decodeVersions)
restClient, err := restclient.UnversionedRESTClientFor(clientConfig)
if err != nil {
return nil, err
}
// TODO(ericchiang): Can we ensure remote service is reachable?
return &WebhookAuthorizer{restClient}, nil
}
// Authorize makes a REST request to the remote service describing the attempted action as a JSON
// serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
// provided bellow.
//
// {
// "apiVersion": "authorization.k8s.io/v1beta1",
// "kind": "SubjectAccessReview",
// "spec": {
// "resourceAttributes": {
// "namespace": "kittensandponies",
// "verb": "GET",
// "group": "group3",
// "resource": "pods"
// },
// "user": "jane",
// "group": [
// "group1",
// "group2"
// ]
// }
// }
//
// The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
// disallow access. A permissive response would return:
//
// {
// "apiVersion": "authorization.k8s.io/v1beta1",
// "kind": "SubjectAccessReview",
// "status": {
// "allowed": true
// }
// }
//
// To disallow access, the remote service would return:
//
// {
// "apiVersion": "authorization.k8s.io/v1beta1",
// "kind": "SubjectAccessReview",
// "status": {
// "allowed": false,
// "reason": "user does not have read access to the namespace"
// }
// }
//
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (err error) {
r := &v1beta1.SubjectAccessReview{
Spec: v1beta1.SubjectAccessReviewSpec{
User: attr.GetUserName(),
Groups: attr.GetGroups(),
},
}
if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &v1beta1.ResourceAttributes{
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Resource: attr.GetResource(),
}
} else {
r.Spec.NonResourceAttributes = &v1beta1.NonResourceAttributes{
Path: attr.GetPath(),
Verb: attr.GetVerb(),
}
}
result := w.restClient.Post().Body(r).Do()
if err := result.Error(); err != nil {
return err
}
if err := result.Into(r); err != nil {
return err
}
if r.Status.Allowed {
return nil
}
if r.Status.Reason != "" {
return errors.New(r.Status.Reason)
}
return errors.New("unauthorized")
}
@@ -0,0 +1,473 @@
/*
Copyright 2016 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 webhook
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"testing"
"text/template"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/user"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api/v1"
"k8s.io/kubernetes/pkg/util"
)
func TestNewFromConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
data := struct {
CA string
Cert string
Key string
}{
CA: filepath.Join(dir, "ca.pem"),
Cert: filepath.Join(dir, "clientcert.pem"),
Key: filepath.Join(dir, "clientkey.pem"),
}
files := []struct {
name string
data []byte
}{
{data.CA, caCert},
{data.Cert, clientCert},
{data.Key, clientKey},
}
for _, file := range files {
if err := ioutil.WriteFile(file.name, file.data, 0400); err != nil {
t.Fatal(err)
}
}
tests := []struct {
msg string
configTmpl string
wantErr bool
}{
{
msg: "a single cluster and single user",
configTmpl: `
clusters:
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: foobar
users:
- name: a cluster
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
`,
wantErr: false,
},
{
msg: "multiple clusters with no context",
configTmpl: `
clusters:
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: foobar
- cluster:
certificate-authority: a bad certificate path
server: https://authz.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
`,
wantErr: false,
},
{
msg: "multiple clusters with a context",
configTmpl: `
clusters:
- cluster:
certificate-authority: a bad certificate path
server: https://authz.example.com
name: foobar
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
contexts:
- name: default
context:
cluster: barfoo
user: a name
current-context: default
`,
wantErr: false,
},
{
msg: "cluster with bad certificate path specified",
configTmpl: `
clusters:
- cluster:
certificate-authority: a bad certificate path
server: https://authz.example.com
name: foobar
- cluster:
certificate-authority: {{ .CA }}
server: https://authz.example.com
name: barfoo
users:
- name: a name
user:
client-certificate: {{ .Cert }}
client-key: {{ .Key }}
contexts:
- name: default
context:
cluster: foobar
user: a name
current-context: default
`,
wantErr: true,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
err := func() error {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return err
}
p := tempfile.Name()
defer os.Remove(p)
tmpl, err := template.New("test").Parse(tt.configTmpl)
if err != nil {
return fmt.Errorf("failed to parse test template: %v", err)
}
if err := tmpl.Execute(tempfile, data); err != nil {
return fmt.Errorf("failed to execute test template: %v", err)
}
// Create a new authorizer
_, err = New(p)
return err
}()
if err != nil && !tt.wantErr {
t.Errorf("failed to load plugin from config %q: %v", tt.msg, err)
}
if err == nil && tt.wantErr {
t.Errorf("wanted an error when loading config, did not get one: %q", tt.msg)
}
}
}
// Service mocks a remote service.
type Service interface {
Review(*v1beta1.SubjectAccessReview)
}
// NewTestServer wraps a Service as an httptest.Server.
func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error) {
var tlsConfig *tls.Config
if cert != nil {
cert, err := tls.X509KeyPair(cert, key)
if err != nil {
return nil, err
}
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
}
if caCert != nil {
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(caCert)
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
tlsConfig.ClientCAs = rootCAs
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
}
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
var review v1beta1.SubjectAccessReview
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
return
}
s.Review(&review)
type status struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason"`
}
resp := struct {
APIVersion string `json:"apiVersion"`
Status status `json:"status"`
}{
APIVersion: v1beta1.SchemeGroupVersion.String(),
Status: status{review.Status.Allowed, review.Status.Reason},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
server.TLS = tlsConfig
server.StartTLS()
return server, nil
}
// A service that can be set to allow all or deny all authorization requests.
type mockService struct {
allow bool
}
func (m *mockService) Review(r *v1beta1.SubjectAccessReview) {
r.Status.Allowed = m.allow
}
func (m *mockService) Allow() { m.allow = true }
func (m *mockService) Deny() { m.allow = false }
// newAuthorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
// a new WebhookAuthorizer from it.
func newAuthorizer(callbackURL string, clientCert, clientKey, ca []byte) (*WebhookAuthorizer, error) {
tempfile, err := ioutil.TempFile("", "")
if err != nil {
return nil, err
}
p := tempfile.Name()
defer os.Remove(p)
config := v1.Config{
Clusters: []v1.NamedCluster{
{
Cluster: v1.Cluster{Server: callbackURL, CertificateAuthorityData: ca},
},
},
AuthInfos: []v1.NamedAuthInfo{
{
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
},
},
}
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
return nil, err
}
return New(p)
}
func TestTLSConfig(t *testing.T) {
tests := []struct {
test string
clientCert, clientKey, clientCA []byte
serverCert, serverKey, serverCA []byte
wantErr bool
}{
{
test: "TLS setup between client and server",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
},
{
test: "Server does not require client auth",
clientCA: caCert,
serverCert: serverCert, serverKey: serverKey,
},
{
test: "Server does not require client auth, client provides it",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey,
},
{
test: "Client does not trust server",
clientCert: clientCert, clientKey: clientKey,
serverCert: serverCert, serverKey: serverKey,
wantErr: true,
},
{
test: "Server does not trust client",
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
wantErr: true,
},
{
// Plugin does not support insecure configurations.
test: "Server is using insecure connection",
wantErr: true,
},
}
for _, tt := range tests {
// Use a closure so defer statements trigger between loop iterations.
func() {
service := new(mockService)
server, err := NewTestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
if err != nil {
t.Errorf("%s: failed to create server: %v", tt.test, err)
return
}
defer server.Close()
wh, err := newAuthorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA)
if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err)
return
}
attr := authorizer.AttributesRecord{User: &user.DefaultInfo{}}
// Allow all and see if we get an error.
service.Allow()
err = wh.Authorize(attr)
if tt.wantErr {
if err == nil {
t.Errorf("expected error making authorization request: %v", err)
}
return
}
if err != nil {
t.Errorf("%s: failed to authorize with AllowAll policy: %v", tt.test, err)
return
}
service.Deny()
if err := wh.Authorize(attr); err == nil {
t.Errorf("%s: incorrectly authorized with DenyAll policy", tt.test)
}
}()
}
}
// recorderService records all access review requests.
type recorderService struct {
last v1beta1.SubjectAccessReview
err error
}
func (rec *recorderService) Review(r *v1beta1.SubjectAccessReview) {
rec.last = v1beta1.SubjectAccessReview{}
rec.last = *r
r.Status.Allowed = true
}
func (rec *recorderService) Last() (v1beta1.SubjectAccessReview, error) {
return rec.last, rec.err
}
func TestWebhook(t *testing.T) {
serv := new(recorderService)
s, err := NewTestServer(serv, serverCert, serverKey, caCert)
if err != nil {
t.Fatal(err)
}
defer s.Close()
wh, err := newAuthorizer(s.URL, clientCert, clientKey, caCert)
if err != nil {
t.Fatal(err)
}
expTypeMeta := unversioned.TypeMeta{
APIVersion: "authorization.k8s.io/v1beta1",
Kind: "SubjectAccessReview",
}
tests := []struct {
attr authorizer.Attributes
want v1beta1.SubjectAccessReview
}{
{
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{}},
want: v1beta1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: v1beta1.SubjectAccessReviewSpec{
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
},
},
},
{
attr: authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "jane"}},
want: v1beta1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: v1beta1.SubjectAccessReviewSpec{
User: "jane",
NonResourceAttributes: &v1beta1.NonResourceAttributes{},
},
},
},
{
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "jane",
UID: "1",
Groups: []string{"group1", "group2"},
},
Verb: "GET",
Namespace: "kittensandponies",
APIGroup: "group3",
Resource: "pods",
ResourceRequest: true,
Path: "/foo",
},
want: v1beta1.SubjectAccessReview{
TypeMeta: expTypeMeta,
Spec: v1beta1.SubjectAccessReviewSpec{
User: "jane",
Groups: []string{"group1", "group2"},
ResourceAttributes: &v1beta1.ResourceAttributes{
Verb: "GET",
Namespace: "kittensandponies",
Group: "group3",
Resource: "pods",
},
},
},
},
}
for i, tt := range tests {
if err := wh.Authorize(tt.attr); err != nil {
t.Errorf("case %d: authorization failed: %v", i, err)
continue
}
gotAttr, err := serv.Last()
if err != nil {
t.Errorf("case %d: failed to deserialize webhook request: %v", i, err)
continue
}
if !reflect.DeepEqual(gotAttr, tt.want) {
t.Errorf("case %d: got != want:\n%s", i, util.ObjectGoPrintDiff(gotAttr, tt.want))
}
}
}
+18
View File
@@ -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 auth contains implementations for interfaces in the pkg/auth package
package auth