mirror of https://github.com/k3s-io/k3s
Add k8s.io/apiserver/plugins/pkg/authenticator from release-1.18
parent
837a943234
commit
acc42874d8
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
Loading…
Reference in New Issue