From acc42874d89851342a1a30db3e9c62ce03ec052b Mon Sep 17 00:00:00 2001 From: Erik Wilson Date: Mon, 24 Aug 2020 09:00:44 -0700 Subject: [PATCH] Add k8s.io/apiserver/plugins/pkg/authenticator from release-1.18 --- pkg/authenticator/basicauth/basicauth.go | 53 ++++++ pkg/authenticator/basicauth/basicauth_test.go | 126 ++++++++++++++ .../passwordfile/passwordfile.go | 95 +++++++++++ .../passwordfile/passwordfile_test.go | 161 ++++++++++++++++++ 4 files changed, 435 insertions(+) create mode 100644 pkg/authenticator/basicauth/basicauth.go create mode 100644 pkg/authenticator/basicauth/basicauth_test.go create mode 100644 pkg/authenticator/passwordfile/passwordfile.go create mode 100644 pkg/authenticator/passwordfile/passwordfile_test.go diff --git a/pkg/authenticator/basicauth/basicauth.go b/pkg/authenticator/basicauth/basicauth.go new file mode 100644 index 0000000000..7b8894a915 --- /dev/null +++ b/pkg/authenticator/basicauth/basicauth.go @@ -0,0 +1,53 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package basicauth + +import ( + "errors" + "net/http" + + "k8s.io/apiserver/pkg/authentication/authenticator" +) + +// 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} +} + +var errInvalidAuth = errors.New("invalid username/password combination") + +// AuthenticateRequest authenticates the request using the "Authorization: Basic" header in the request +func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + username, password, found := req.BasicAuth() + if !found { + return nil, false, nil + } + + resp, ok, err := a.auth.AuthenticatePassword(req.Context(), username, password) + + // If the password authenticator didn't error, provide a default error + if !ok && err == nil { + err = errInvalidAuth + } + + return resp, ok, err +} diff --git a/pkg/authenticator/basicauth/basicauth_test.go b/pkg/authenticator/basicauth/basicauth_test.go new file mode 100644 index 0000000000..c23e0ce082 --- /dev/null +++ b/pkg/authenticator/basicauth/basicauth_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package basicauth + +import ( + "context" + "errors" + "net/http" + "testing" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +type testPassword struct { + Username string + Password string + Called bool + + User user.Info + OK bool + Err error +} + +func (t *testPassword) AuthenticatePassword(ctx context.Context, user, password string) (*authenticator.Response, bool, error) { + t.Called = true + t.Username = user + t.Password = password + return &authenticator.Response{User: 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: "", + ExpectedErr: true, + }, + "valid basic header": { + ExpectedCalled: true, + ExpectedUsername: "myuser", + ExpectedPassword: "mypassword:withcolon", + ExpectedErr: true, + }, + "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) + } + + resp, 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 != resp.User.GetName() { + t.Errorf("%s: Expected user.GetName()=%v, got %v", k, testCase.ExpectedUser, resp.User.GetName()) + continue + } + } +} diff --git a/pkg/authenticator/passwordfile/passwordfile.go b/pkg/authenticator/passwordfile/passwordfile.go new file mode 100644 index 0000000000..e17a5eb0de --- /dev/null +++ b/pkg/authenticator/passwordfile/passwordfile.go @@ -0,0 +1,95 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package passwordfile + +import ( + "context" + "crypto/subtle" + "encoding/csv" + "fmt" + "io" + "os" + "strings" + + "k8s.io/klog" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +// PasswordAuthenticator authenticates users by password +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() + + recordNum := 0 + users := make(map[string]*userPasswordInfo) + 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("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], + } + if len(record) >= 4 { + obj.info.Groups = strings.Split(record[3], ",") + } + recordNum++ + if _, exist := users[obj.info.Name]; exist { + klog.Warningf("duplicate username '%s' has been found in password file '%s', record number '%d'", obj.info.Name, path, recordNum) + } + users[obj.info.Name] = obj + } + + return &PasswordAuthenticator{users}, nil +} + +// AuthenticatePassword returns user info if authentication is successful, nil otherwise +func (a *PasswordAuthenticator) AuthenticatePassword(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { + user, ok := a.users[username] + if !ok { + return nil, false, nil + } + if subtle.ConstantTimeCompare([]byte(user.password), []byte(password)) == 0 { + return nil, false, nil + } + return &authenticator.Response{User: user.info}, true, nil +} diff --git a/pkg/authenticator/passwordfile/passwordfile_test.go b/pkg/authenticator/passwordfile/passwordfile_test.go new file mode 100644 index 0000000000..e9f43d6dcb --- /dev/null +++ b/pkg/authenticator/passwordfile/passwordfile_test.go @@ -0,0 +1,161 @@ +/* +Copyright 2015 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package passwordfile + +import ( + "context" + "io/ioutil" + "os" + "reflect" + "testing" + + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestPasswordFile(t *testing.T) { + auth, err := newWithContents(t, ` +password1,user1,uid1 +password2,user2,uid2 +password3,user3,uid3,"group1,group2" +password4,user4,uid4,"group2" +password5,user5,uid5,group5 +password6,user6,uid6,group5,otherdata +password7,user7,uid7,"group1,group2",otherdata +`) + 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", + User: &user.DefaultInfo{Name: "user3", UID: "uid3", Groups: []string{"group1", "group2"}}, + Ok: true, + }, + { + Username: "user4", + Password: "password4", + User: &user.DefaultInfo{Name: "user4", UID: "uid4", Groups: []string{"group2"}}, + Ok: true, + }, + { + Username: "user5", + Password: "password5", + User: &user.DefaultInfo{Name: "user5", UID: "uid5", Groups: []string{"group5"}}, + Ok: true, + }, + { + Username: "user6", + Password: "password6", + User: &user.DefaultInfo{Name: "user6", UID: "uid6", Groups: []string{"group5"}}, + Ok: true, + }, + { + Username: "user7", + Password: "password7", + User: &user.DefaultInfo{Name: "user7", UID: "uid7", Groups: []string{"group1", "group2"}}, + Ok: true, + }, + { + Username: "user7", + Password: "passwordbad", + }, + { + Username: "userbad", + Password: "password7", + }, + { + Username: "user8", + Password: "password8", + }, + } + for i, testCase := range testCases { + resp, ok, err := auth.AuthenticatePassword(context.Background(), testCase.Username, testCase.Password) + if err != nil { + t.Errorf("%d: unexpected error: %v", i, err) + } + if testCase.User == nil { + if resp != nil { + t.Errorf("%d: unexpected non-nil user %#v", i, resp.User) + } + } else if !reflect.DeepEqual(testCase.User, resp.User) { + t.Errorf("%d: expected user %#v, got %#v", i, testCase.User, resp.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()) +}