mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-28 16:00:19 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc31d467e4 | ||
|
|
6ae8e36683 | ||
|
|
356f0d519d | ||
|
|
e84d29bc6b | ||
|
|
ae80ebf148 | ||
|
|
5942c82b5f | ||
|
|
a5f9c698ea | ||
|
|
40ef2c25b8 | ||
|
|
2422c46271 | ||
|
|
6ca0ee8013 | ||
|
|
9ac252667a |
@@ -2,7 +2,7 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.11.1
|
||||
- image: circleci/golang:1.12.3
|
||||
steps:
|
||||
- run: |
|
||||
mkdir -p ~/bin
|
||||
@@ -10,12 +10,13 @@ jobs:
|
||||
- run: |
|
||||
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
|
||||
chmod +x ~/bin/kubectl
|
||||
- run: |
|
||||
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.3.0/ghcp_linux_amd64
|
||||
chmod +x ~/bin/ghcp
|
||||
- run: |
|
||||
go get -v \
|
||||
golang.org/x/lint/golint \
|
||||
github.com/int128/goxzst \
|
||||
github.com/tcnksm/ghr \
|
||||
github.com/int128/ghcp
|
||||
github.com/tcnksm/ghr
|
||||
- checkout
|
||||
# workaround for https://github.com/golang/go/issues/27925
|
||||
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
|
||||
|
||||
5
Makefile
5
Makefile
@@ -8,10 +8,9 @@ LDFLAGS := -X main.version=$(CIRCLE_TAG)
|
||||
all: $(TARGET)
|
||||
|
||||
check:
|
||||
golint
|
||||
go vet
|
||||
$(MAKE) -C adaptors_test/authserver/testdata
|
||||
go test -v ./...
|
||||
$(MAKE) -C adaptors_test/keys/testdata
|
||||
go test -v -race ./...
|
||||
|
||||
$(TARGET): $(wildcard *.go)
|
||||
go build -o $@ -ldflags "$(LDFLAGS)"
|
||||
|
||||
19
README.md
19
README.md
@@ -13,19 +13,18 @@ You need to setup the following components:
|
||||
- Role for your group or user
|
||||
- kubectl authentication
|
||||
|
||||
You can install [the latest release](https://github.com/int128/kubelogin/releases) by the following ways:
|
||||
You can install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
|
||||
|
||||
```sh
|
||||
# Homebrew
|
||||
brew tap int128/kubelogin
|
||||
brew install kubelogin
|
||||
|
||||
# Krew (experimental)
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.0/oidc-login.yaml
|
||||
kubectl krew install --manifest oidc-login.yaml
|
||||
# Krew
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.0/kubelogin_linux_amd64.zip
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.1/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
@@ -34,11 +33,10 @@ After initial setup or when the token has been expired, just run:
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/08/27 15:03:06 Reading /home/user/.kube/config
|
||||
2018/08/27 15:03:06 Using current context: hello.k8s.local
|
||||
2018/08/27 15:03:07 Open http://localhost:8000 for authorization
|
||||
2018/08/27 15:03:09 Got token for subject=cf228a73-47fe-4986-a2a8-b2ced80a884b
|
||||
2018/08/27 15:03:09 Updated /home/user/.kube/config
|
||||
Using current-context: hello.k8s.local
|
||||
Open http://localhost:8000 for authorization
|
||||
Got a token for subject 0123456789 (valid until 2019-04-12 11:00:49 +0900 JST)
|
||||
Updated ~/.kube/config
|
||||
```
|
||||
|
||||
or run as a kubectl plugin:
|
||||
@@ -70,6 +68,7 @@ Application Options:
|
||||
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
|
||||
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
|
||||
-v, --v= If set to 1 or greater, show debug log (default: 0)
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
|
||||
@@ -30,16 +30,17 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
version)
|
||||
args, err := parser.ParseArgs(args[1:])
|
||||
if err != nil {
|
||||
cmd.Logger.Logf("Error: %s", err)
|
||||
cmd.Logger.Printf("Error: %s", err)
|
||||
return 1
|
||||
}
|
||||
if len(args) > 0 {
|
||||
cmd.Logger.Logf("Error: too many arguments")
|
||||
cmd.Logger.Printf("Error: too many arguments")
|
||||
return 1
|
||||
}
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
kubeConfig, err := o.ExpandKubeConfig()
|
||||
if err != nil {
|
||||
cmd.Logger.Logf("Error: invalid option: %s", err)
|
||||
cmd.Logger.Printf("Error: invalid option: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
}
|
||||
if err := cmd.Login.Do(ctx, in); err != nil {
|
||||
cmd.Logger.Logf("Error: %s", err)
|
||||
cmd.Logger.Printf("Error: %s", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
@@ -61,6 +62,7 @@ type cmdOptions struct {
|
||||
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
|
||||
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
|
||||
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
|
||||
Verbose int `long:"v" short:"v" default:"0" description:"If set to 1 or greater, show debug log"`
|
||||
}
|
||||
|
||||
// ExpandKubeConfig returns an expanded KubeConfig path.
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
@@ -26,9 +28,13 @@ func TestCmd_Run(t *testing.T) {
|
||||
ListenPort: 8000,
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
Login: login,
|
||||
Logger: t,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable}, version)
|
||||
if exitCode != 0 {
|
||||
@@ -50,14 +56,19 @@ func TestCmd_Run(t *testing.T) {
|
||||
SkipOpenBrowser: true,
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
SetLevel(adaptors.LogLevel(1))
|
||||
|
||||
cmd := Cmd{
|
||||
Login: login,
|
||||
Logger: t,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"--listen-port", "10080",
|
||||
"--insecure-skip-tls-verify",
|
||||
"--skip-open-browser",
|
||||
"-v1",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
@@ -69,7 +80,7 @@ func TestCmd_Run(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
cmd := Cmd{
|
||||
Login: mock_usecases.NewMockLogin(ctrl),
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
|
||||
if exitCode != 1 {
|
||||
|
||||
@@ -8,14 +8,19 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/infrastructure"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func NewHTTP() adaptors.HTTP {
|
||||
return &HTTP{}
|
||||
func NewHTTP(i HTTP) adaptors.HTTP {
|
||||
return &i
|
||||
}
|
||||
|
||||
type HTTP struct{}
|
||||
type HTTP struct {
|
||||
dig.In
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
|
||||
return &httpClientConfig{
|
||||
@@ -23,11 +28,14 @@ func (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func (*HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
|
||||
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: config.TLSConfig(),
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Transport: &infrastructure.LoggingTransport{
|
||||
Base: &http.Transport{
|
||||
TLSClientConfig: config.TLSConfig(),
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Logger: h.Logger,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,HTTPClientConfig,OIDC
|
||||
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger
|
||||
|
||||
type Cmd interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
@@ -34,7 +34,8 @@ type HTTPClientConfig interface {
|
||||
}
|
||||
|
||||
type OIDC interface {
|
||||
Authenticate(ctx context.Context, in OIDCAuthenticateIn) (*OIDCAuthenticateOut, error)
|
||||
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
|
||||
VerifyIDToken(ctx context.Context, in OIDCVerifyTokenIn) (*oidc.IDToken, error)
|
||||
}
|
||||
|
||||
type OIDCAuthenticateIn struct {
|
||||
@@ -47,12 +48,35 @@ type OIDCAuthenticateIn struct {
|
||||
SkipOpenBrowser bool // skip opening browser if true
|
||||
}
|
||||
|
||||
type OIDCAuthenticateCallback struct {
|
||||
ShowLocalServerURL func(url string)
|
||||
}
|
||||
|
||||
type OIDCAuthenticateOut struct {
|
||||
VerifiedIDToken *oidc.IDToken
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Logf(format string, v ...interface{})
|
||||
type OIDCVerifyTokenIn struct {
|
||||
IDToken string
|
||||
Issuer string
|
||||
ClientID string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Debugf(level LogLevel, format string, v ...interface{})
|
||||
SetLevel(level LogLevel)
|
||||
IsEnabled(level LogLevel) bool
|
||||
}
|
||||
|
||||
// LogLevel represents a log level for debug.
|
||||
//
|
||||
// 0 = None
|
||||
// 1 = Including in/out
|
||||
// 2 = Including transport headers
|
||||
// 3 = Including transport body
|
||||
//
|
||||
type LogLevel int
|
||||
|
||||
@@ -2,16 +2,48 @@ package adaptors
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
// NewLogger returns a Logger with the standard log.Logger for messages and debug.
|
||||
func NewLogger() adaptors.Logger {
|
||||
return &Logger{}
|
||||
return &Logger{
|
||||
stdLogger: log.New(os.Stderr, "", 0),
|
||||
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
|
||||
}
|
||||
}
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
func (*Logger) Logf(format string, v ...interface{}) {
|
||||
log.Printf(format, v...)
|
||||
// NewLoggerWith returns a Logger with the given standard log.Logger.
|
||||
func NewLoggerWith(l stdLogger) *Logger {
|
||||
return &Logger{
|
||||
stdLogger: l,
|
||||
debugLogger: l,
|
||||
}
|
||||
}
|
||||
|
||||
type stdLogger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger wraps the standard log.Logger and just provides debug level.
|
||||
type Logger struct {
|
||||
stdLogger
|
||||
debugLogger stdLogger
|
||||
level adaptors.LogLevel
|
||||
}
|
||||
|
||||
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
|
||||
if l.IsEnabled(level) {
|
||||
l.debugLogger.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) SetLevel(level adaptors.LogLevel) {
|
||||
l.level = level
|
||||
}
|
||||
|
||||
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
|
||||
return level <= l.level
|
||||
}
|
||||
|
||||
53
adaptors/logger_test.go
Normal file
53
adaptors/logger_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
type mockStdLogger struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
|
||||
l.count++
|
||||
}
|
||||
|
||||
func TestLogger_Debugf(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
loggerLevel adaptors.LogLevel
|
||||
debugfLevel adaptors.LogLevel
|
||||
count int
|
||||
}{
|
||||
{0, 0, 1},
|
||||
{0, 1, 0},
|
||||
|
||||
{1, 0, 1},
|
||||
{1, 1, 1},
|
||||
{1, 2, 0},
|
||||
|
||||
{2, 1, 1},
|
||||
{2, 2, 1},
|
||||
{2, 3, 0},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%+v", c), func(t *testing.T) {
|
||||
m := &mockStdLogger{}
|
||||
l := &Logger{debugLogger: m, level: c.loggerLevel}
|
||||
l.Debugf(c.debugfLevel, "hello")
|
||||
if m.count != c.count {
|
||||
t.Errorf("count wants %d but %d", c.count, m.count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogger_Printf(t *testing.T) {
|
||||
m := &mockStdLogger{}
|
||||
l := &Logger{stdLogger: m}
|
||||
l.Printf("hello")
|
||||
if m.count != 1 {
|
||||
t.Errorf("count wants %d but %d", 1, m.count)
|
||||
}
|
||||
}
|
||||
31
adaptors/mock_adaptors/logger.go
Normal file
31
adaptors/mock_adaptors/logger.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package mock_adaptors
|
||||
|
||||
import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {
|
||||
return &Logger{
|
||||
MockLogger: NewMockLogger(ctrl),
|
||||
testingLogger: t,
|
||||
}
|
||||
}
|
||||
|
||||
type testingLogger interface {
|
||||
Logf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger provides mock feature but overrides output methods with actual logging.
|
||||
type Logger struct {
|
||||
*MockLogger
|
||||
testingLogger testingLogger
|
||||
}
|
||||
|
||||
func (l *Logger) Printf(format string, v ...interface{}) {
|
||||
l.testingLogger.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
|
||||
l.testingLogger.Logf(format, v...)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,OIDC)
|
||||
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger)
|
||||
|
||||
// Package mock_adaptors is a generated GoMock package.
|
||||
package mock_adaptors
|
||||
@@ -7,6 +7,7 @@ package mock_adaptors
|
||||
import (
|
||||
context "context"
|
||||
tls "crypto/tls"
|
||||
go_oidc "github.com/coreos/go-oidc"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
|
||||
api "k8s.io/client-go/tools/clientcmd/api"
|
||||
@@ -203,14 +204,102 @@ func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
|
||||
}
|
||||
|
||||
// Authenticate mocks base method
|
||||
func (m *MockOIDC) Authenticate(arg0 context.Context, arg1 interfaces.OIDCAuthenticateIn) (*interfaces.OIDCAuthenticateOut, error) {
|
||||
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1)
|
||||
func (m *MockOIDC) Authenticate(arg0 context.Context, arg1 interfaces.OIDCAuthenticateIn, arg2 interfaces.OIDCAuthenticateCallback) (*interfaces.OIDCAuthenticateOut, error) {
|
||||
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*interfaces.OIDCAuthenticateOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Authenticate indicates an expected call of Authenticate
|
||||
func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1)
|
||||
func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// VerifyIDToken mocks base method
|
||||
func (m *MockOIDC) VerifyIDToken(arg0 context.Context, arg1 interfaces.OIDCVerifyTokenIn) (*go_oidc.IDToken, error) {
|
||||
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
|
||||
ret0, _ := ret[0].(*go_oidc.IDToken)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// VerifyIDToken indicates an expected call of VerifyIDToken
|
||||
func (mr *MockOIDCMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockOIDC)(nil).VerifyIDToken), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockLogger is a mock of Logger interface
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debugf mocks base method
|
||||
func (m *MockLogger) Debugf(arg0 interfaces.LogLevel, arg1 string, arg2 ...interface{}) {
|
||||
varargs := []interface{}{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Debugf", varargs...)
|
||||
}
|
||||
|
||||
// Debugf indicates an expected call of Debugf
|
||||
func (mr *MockLoggerMockRecorder) Debugf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
|
||||
}
|
||||
|
||||
// IsEnabled mocks base method
|
||||
func (m *MockLogger) IsEnabled(arg0 interfaces.LogLevel) bool {
|
||||
ret := m.ctrl.Call(m, "IsEnabled", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsEnabled indicates an expected call of IsEnabled
|
||||
func (mr *MockLoggerMockRecorder) IsEnabled(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockLogger)(nil).IsEnabled), arg0)
|
||||
}
|
||||
|
||||
// Printf mocks base method
|
||||
func (m *MockLogger) Printf(arg0 string, arg1 ...interface{}) {
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Printf", varargs...)
|
||||
}
|
||||
|
||||
// Printf indicates an expected call of Printf
|
||||
func (mr *MockLoggerMockRecorder) Printf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Printf", reflect.TypeOf((*MockLogger)(nil).Printf), varargs...)
|
||||
}
|
||||
|
||||
// SetLevel mocks base method
|
||||
func (m *MockLogger) SetLevel(arg0 interfaces.LogLevel) {
|
||||
m.ctrl.Call(m, "SetLevel", arg0)
|
||||
}
|
||||
|
||||
// SetLevel indicates an expected call of SetLevel
|
||||
func (mr *MockLoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*MockLogger)(nil).SetLevel), arg0)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func NewOIDC() adaptors.OIDC {
|
||||
|
||||
type OIDC struct{}
|
||||
|
||||
func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
if in.Client != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
|
||||
}
|
||||
@@ -31,9 +31,10 @@ func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn) (
|
||||
ClientSecret: in.ClientSecret,
|
||||
Scopes: append(in.ExtraScopes, oidc.ScopeOpenID),
|
||||
},
|
||||
LocalServerPort: in.LocalServerPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
|
||||
LocalServerPort: in.LocalServerPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
|
||||
ShowLocalServerURL: cb.ShowLocalServerURL,
|
||||
}
|
||||
token, err := flow.GetToken(ctx)
|
||||
if err != nil {
|
||||
@@ -54,3 +55,19 @@ func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn) (
|
||||
RefreshToken: token.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*OIDC) VerifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) (*oidc.IDToken, error) {
|
||||
if in.Client != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
|
||||
}
|
||||
provider, err := oidc.NewProvider(ctx, in.Issuer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
|
||||
}
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not verify the id_token")
|
||||
}
|
||||
return verifiedIDToken, nil
|
||||
}
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
package authserver
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Addr is address to listen.
|
||||
const Addr = "localhost:9000"
|
||||
|
||||
// CACert is path to the CA certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const CACert = "authserver/testdata/ca.crt"
|
||||
|
||||
// ServerCert is path to the server certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const ServerCert = "authserver/testdata/server.crt"
|
||||
|
||||
// ServerKey is path to the server key.
|
||||
// This should be generated by Makefile before test.
|
||||
const ServerKey = "authserver/testdata/server.key"
|
||||
|
||||
// Config represents server configuration.
|
||||
type Config struct {
|
||||
Issuer string
|
||||
Scope string
|
||||
Cert string
|
||||
Key string
|
||||
Issuer string
|
||||
Scope string
|
||||
TLSServerCert string
|
||||
TLSServerKey string
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenKeyPair *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// Start starts a HTTP server.
|
||||
func (c *Config) Start(t *testing.T) *http.Server {
|
||||
func Start(t *testing.T, c Config) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: Addr,
|
||||
Addr: "localhost:9000",
|
||||
Handler: newHandler(t, c),
|
||||
}
|
||||
go func() {
|
||||
var err error
|
||||
if c.Cert != "" && c.Key != "" {
|
||||
err = s.ListenAndServeTLS(c.Cert, c.Key)
|
||||
if c.TLSServerCert != "" && c.TLSServerKey != "" {
|
||||
err = s.ListenAndServeTLS(c.TLSServerCert, c.TLSServerKey)
|
||||
} else {
|
||||
err = s.ListenAndServe()
|
||||
}
|
||||
|
||||
@@ -1,76 +1,65 @@
|
||||
package authserver
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
t *testing.T
|
||||
|
||||
discovery *template.Template
|
||||
token *template.Template
|
||||
jwks *template.Template
|
||||
authCode string
|
||||
|
||||
Issuer string
|
||||
Scope string // Default to openid
|
||||
IDToken string
|
||||
PrivateKey struct{ N, E string }
|
||||
// Template values
|
||||
Issuer string
|
||||
Scope string // Default to openid
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
PrivateKey struct{ N, E string }
|
||||
}
|
||||
|
||||
func newHandler(t *testing.T, c *Config) *handler {
|
||||
func newHandler(t *testing.T, c Config) *handler {
|
||||
tpl, err := template.ParseFiles(
|
||||
"authserver/testdata/oidc-discovery.json",
|
||||
"authserver/testdata/oidc-token.json",
|
||||
"authserver/testdata/oidc-jwks.json",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the templates: %s", err)
|
||||
}
|
||||
h := handler{
|
||||
discovery: readTemplate(t, "oidc-discovery.json"),
|
||||
token: readTemplate(t, "oidc-token.json"),
|
||||
jwks: readTemplate(t, "oidc-jwks.json"),
|
||||
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
|
||||
Issuer: c.Issuer,
|
||||
Scope: c.Scope,
|
||||
t: t,
|
||||
discovery: tpl.Lookup("oidc-discovery.json"),
|
||||
token: tpl.Lookup("oidc-token.json"),
|
||||
jwks: tpl.Lookup("oidc-jwks.json"),
|
||||
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
|
||||
Issuer: c.Issuer,
|
||||
Scope: c.Scope,
|
||||
IDToken: c.IDToken,
|
||||
RefreshToken: c.RefreshToken,
|
||||
}
|
||||
if h.Scope == "" {
|
||||
h.Scope = "openid"
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
|
||||
Issuer: c.Issuer,
|
||||
Audience: "kubernetes",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
k, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate a key pair: %s", err)
|
||||
if c.IDTokenKeyPair != nil {
|
||||
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
|
||||
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
|
||||
}
|
||||
h.IDToken, err = token.SignedString(k)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not generate an ID token: %s", err)
|
||||
}
|
||||
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
|
||||
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
|
||||
return &h
|
||||
}
|
||||
|
||||
func readTemplate(t *testing.T, name string) *template.Template {
|
||||
t.Helper()
|
||||
tpl, err := template.ParseFiles("authserver/testdata/" + name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read template %s: %s", name, err)
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.serveHTTP(w, r); err != nil {
|
||||
log.Printf("[auth-server] Error: %s", err)
|
||||
h.t.Logf("[auth-server] Error: %s", err)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
@@ -78,12 +67,12 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
m := r.Method
|
||||
p := r.URL.Path
|
||||
log.Printf("[auth-server] %s %s", m, r.RequestURI)
|
||||
h.t.Logf("[auth-server] %s %s", m, r.RequestURI)
|
||||
switch {
|
||||
case m == "GET" && p == "/.well-known/openid-configuration":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := h.discovery.Execute(w, h); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "could not execute the template")
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/auth":
|
||||
// Authentication Response
|
||||
@@ -98,19 +87,19 @@ func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// Token Response
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "could not parse the form")
|
||||
}
|
||||
if h.authCode != r.Form.Get("code") {
|
||||
return errors.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := h.token.Execute(w, h); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "could not execute the template")
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/certs":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := h.jwks.Execute(w, h); err != nil {
|
||||
return err
|
||||
return errors.Wrapf(err, "could not execute the template")
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Not Found", 404)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": "44df4c82-5ce7-4260-b54d-1da0d396ef2a",
|
||||
"refresh_token": "{{ .RefreshToken }}",
|
||||
"expires_in": 3600,
|
||||
"id_token": "{{ .IDToken }}"
|
||||
}
|
||||
|
||||
@@ -3,170 +3,228 @@ package adaptors_test
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors_test/authserver"
|
||||
"github.com/int128/kubelogin/adaptors_test/keys"
|
||||
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors_test/logger"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// End-to-end test.
|
||||
// Run the integration tests.
|
||||
//
|
||||
// 1. Start the auth server at port 9000.
|
||||
// 2. Run the CLI.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for port 8000.
|
||||
// 4. Wait for the CLI.
|
||||
// 4. Wait for the Cmd.
|
||||
// 5. Shutdown the auth server.
|
||||
//
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
data := map[string]struct {
|
||||
kubeconfigValues kubeconfig.Values
|
||||
args []string
|
||||
serverConfig authserver.Config
|
||||
clientTLS *tls.Config
|
||||
}{
|
||||
"NoTLS": {
|
||||
kubeconfig.Values{Issuer: "http://localhost:9000"},
|
||||
[]string{"kubelogin"},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
"ExtraScope": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
ExtraScopes: "profile groups",
|
||||
},
|
||||
[]string{"kubelogin"},
|
||||
authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
Scope: "profile groups openid",
|
||||
},
|
||||
&tls.Config{},
|
||||
},
|
||||
"SkipTLSVerify": {
|
||||
kubeconfig.Values{Issuer: "https://localhost:9000"},
|
||||
[]string{"kubelogin", "--insecure-skip-tls-verify"},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
Cert: authserver.ServerCert,
|
||||
Key: authserver.ServerKey,
|
||||
},
|
||||
&tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
"CACert": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: authserver.CACert,
|
||||
},
|
||||
[]string{"kubelogin"},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
Cert: authserver.ServerCert,
|
||||
Key: authserver.ServerKey,
|
||||
},
|
||||
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
|
||||
},
|
||||
"CACertData": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString(read(t, authserver.CACert)),
|
||||
},
|
||||
[]string{"kubelogin"},
|
||||
authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
Cert: authserver.ServerCert,
|
||||
Key: authserver.ServerKey,
|
||||
},
|
||||
&tls.Config{RootCAs: readCert(t, authserver.CACert)},
|
||||
},
|
||||
"InvalidCACertShouldBeSkipped": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDPCertificateAuthority: "cmd_test.go",
|
||||
},
|
||||
[]string{"kubelogin"},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
"InvalidCACertDataShouldBeSkipped": {
|
||||
kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDPCertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("foo")),
|
||||
},
|
||||
[]string{"kubelogin"},
|
||||
authserver.Config{Issuer: "http://localhost:9000"},
|
||||
&tls.Config{},
|
||||
},
|
||||
}
|
||||
timeout := 1 * time.Second
|
||||
|
||||
for name, c := range data {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
server := c.serverConfig.Start(t)
|
||||
defer server.Shutdown(ctx)
|
||||
kcfg := kubeconfig.Create(t, &c.kubeconfigValues)
|
||||
defer os.Remove(kcfg)
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
args := append(c.args, "--kubeconfig", kcfg, "--skip-open-browser")
|
||||
var eg errgroup.Group
|
||||
eg.Go(func() error {
|
||||
return di.Invoke(func(cmd adaptors.Cmd) {
|
||||
exitCode := cmd.Run(ctx, args, "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
if err := openBrowserRequest(c.clientTLS); err != nil {
|
||||
cancel()
|
||||
t.Error(err)
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
t.Fatalf("CLI returned error: %s", err)
|
||||
}
|
||||
kubeconfig.Verify(t, kcfg)
|
||||
idToken := newIDToken(t, "http://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "http://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
Scope: "profile groups openid",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACert", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "https://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
TLSServerCert: keys.TLSServerCert,
|
||||
TLSServerKey: keys.TLSServerKey,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthority: keys.TLSCACert,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACertData", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "https://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
TLSServerCert: keys.TLSServerCert,
|
||||
TLSServerKey: keys.TLSServerKey,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
|
||||
idToken := newIDToken(t, "http://localhost:9000")
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDToken: idToken,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer string) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
}
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func runCmd(t *testing.T, ctx context.Context, args ...string) {
|
||||
t.Helper()
|
||||
newLogger := func() adaptors.Logger {
|
||||
return logger.New(t)
|
||||
}
|
||||
if err := di.InvokeWithExtra(func(cmd adaptors.Cmd) {
|
||||
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}, newLogger); err != nil {
|
||||
t.Errorf("Invoke returned error: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func openBrowserRequest(tlsConfig *tls.Config) error {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) {
|
||||
t.Helper()
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
res, err := client.Get("http://localhost:8000/")
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/", nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not send a request")
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return errors.Errorf("StatusCode wants 200 but %d", res.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func read(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not read %s: %s", name, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func readCert(t *testing.T, name string) *x509.CertPool {
|
||||
t.Helper()
|
||||
p := x509.NewCertPool()
|
||||
b := read(t, name)
|
||||
if !p.AppendCertsFromPEM(b) {
|
||||
t.Fatalf("Could not append cert from %s", name)
|
||||
}
|
||||
return p
|
||||
req = req.WithContext(ctx)
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
71
adaptors_test/keys/keys.go
Normal file
71
adaptors_test/keys/keys.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package keys
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// TLSCACert is path to the CA certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const TLSCACert = "keys/testdata/ca.crt"
|
||||
|
||||
// TLSCACertAsBase64 is a base64 encoded string of TLSCACert.
|
||||
var TLSCACertAsBase64 string
|
||||
|
||||
// TLSCACertAsConfig is a TLS config including TLSCACert.
|
||||
var TLSCACertAsConfig = &tls.Config{RootCAs: x509.NewCertPool()}
|
||||
|
||||
// TLSServerCert is path to the server certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const TLSServerCert = "keys/testdata/server.crt"
|
||||
|
||||
// TLSServerKey is path to the server key.
|
||||
// This should be generated by Makefile before test.
|
||||
const TLSServerKey = "keys/testdata/server.key"
|
||||
|
||||
// JWSKey is path to the key for signing ID tokens.
|
||||
const JWSKey = "keys/testdata/jws.key"
|
||||
|
||||
// JWSKeyPair is the key pair loaded from JWSKey.
|
||||
var JWSKeyPair *rsa.PrivateKey
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
JWSKeyPair, err = readPrivateKey(JWSKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(TLSCACert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
TLSCACertAsBase64 = base64.StdEncoding.EncodeToString(b)
|
||||
if !TLSCACertAsConfig.RootCAs.AppendCertsFromPEM(b) {
|
||||
panic("could not append the CA cert")
|
||||
}
|
||||
}
|
||||
|
||||
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read JWSKey")
|
||||
}
|
||||
block, rest := pem.Decode(b)
|
||||
if block == nil {
|
||||
return nil, errors.New("could not decode PEM")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("PEM should contain single key but multiple keys")
|
||||
}
|
||||
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse the key")
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: server.crt ca.crt
|
||||
all: server.crt ca.crt jws.key
|
||||
|
||||
clean:
|
||||
rm -v ca.* server.*
|
||||
@@ -48,3 +48,6 @@ server.crt: openssl.cnf server.csr ca.key ca.crt
|
||||
-in server.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
jws.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
@@ -3,8 +3,10 @@ package kubeconfig
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Values represents values in .kubeconfig template.
|
||||
@@ -13,6 +15,7 @@ type Values struct {
|
||||
ExtraScopes string
|
||||
IDPCertificateAuthority string
|
||||
IDPCertificateAuthorityData string
|
||||
IDToken string
|
||||
}
|
||||
|
||||
// Create creates a kubeconfig file and returns path to it.
|
||||
@@ -33,16 +36,44 @@ func Create(t *testing.T, v *Values) string {
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
type AuthProviderConfig struct {
|
||||
IDToken string `yaml:"id-token"`
|
||||
RefreshToken string `yaml:"refresh-token"`
|
||||
}
|
||||
|
||||
// Verify returns true if the kubeconfig has valid values.
|
||||
func Verify(t *testing.T, kubeconfig string) {
|
||||
b, err := ioutil.ReadFile(kubeconfig)
|
||||
func Verify(t *testing.T, kubeconfig string, want AuthProviderConfig) {
|
||||
t.Helper()
|
||||
f, err := os.Open(kubeconfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Errorf("could not open kubeconfig: %s", err)
|
||||
return
|
||||
}
|
||||
if strings.Index(string(b), "id-token: ey") == -1 {
|
||||
t.Errorf("kubeconfig wants id-token but %s", string(b))
|
||||
defer f.Close()
|
||||
|
||||
var y struct {
|
||||
Users []struct {
|
||||
User struct {
|
||||
AuthProvider struct {
|
||||
Config AuthProviderConfig `yaml:"config"`
|
||||
} `yaml:"auth-provider"`
|
||||
} `yaml:"user"`
|
||||
} `yaml:"users"`
|
||||
}
|
||||
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
|
||||
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
|
||||
d := yaml.NewDecoder(f)
|
||||
if err := d.Decode(&y); err != nil {
|
||||
t.Errorf("could not decode YAML: %s", err)
|
||||
return
|
||||
}
|
||||
if len(y.Users) != 1 {
|
||||
t.Errorf("len(users) wants 1 but %d", len(y.Users))
|
||||
return
|
||||
}
|
||||
currentConfig := y.Users[0].User.AuthProvider.Config
|
||||
if currentConfig.IDToken != want.IDToken {
|
||||
t.Errorf("id-token wants %s but %s", want.IDToken, currentConfig.IDToken)
|
||||
}
|
||||
if currentConfig.RefreshToken != want.RefreshToken {
|
||||
t.Errorf("refresh-token wants %s but %s", want.RefreshToken, currentConfig.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,8 @@ users:
|
||||
#{{ end }}
|
||||
#{{ if .IDPCertificateAuthorityData }}
|
||||
idp-certificate-authority-data: {{ .IDPCertificateAuthorityData }}
|
||||
#{{ end }}
|
||||
#{{ if .IDToken }}
|
||||
id-token: {{ .IDToken }}
|
||||
#{{ end }}
|
||||
name: oidc
|
||||
|
||||
21
adaptors_test/logger/logger.go
Normal file
21
adaptors_test/logger/logger.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
func New(t testingLogger) *adaptors.Logger {
|
||||
return adaptors.NewLoggerWith(&bridge{t})
|
||||
}
|
||||
|
||||
type testingLogger interface {
|
||||
Logf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type bridge struct {
|
||||
t testingLogger
|
||||
}
|
||||
|
||||
func (b *bridge) Printf(format string, v ...interface{}) {
|
||||
b.t.Logf(format, v...)
|
||||
}
|
||||
12
di/di.go
12
di/di.go
@@ -16,13 +16,21 @@ var constructors = []interface{}{
|
||||
adaptors.NewKubeConfig,
|
||||
adaptors.NewOIDC,
|
||||
adaptors.NewHTTP,
|
||||
}
|
||||
|
||||
var extraConstructors = []interface{}{
|
||||
adaptors.NewLogger,
|
||||
}
|
||||
|
||||
// Invoke runs the function with an adaptors.Cmd instance.
|
||||
// Invoke runs the function with the default constructors.
|
||||
func Invoke(f func(cmd adaptorsInterfaces.Cmd)) error {
|
||||
return InvokeWithExtra(f, extraConstructors...)
|
||||
}
|
||||
|
||||
// InvokeWithExtra runs the function with the given constructors.
|
||||
func InvokeWithExtra(f func(cmd adaptorsInterfaces.Cmd), extra ...interface{}) error {
|
||||
c := dig.New()
|
||||
for _, constructor := range constructors {
|
||||
for _, constructor := range append(constructors, extra...) {
|
||||
if err := c.Provide(constructor); err != nil {
|
||||
return errors.Wrapf(err, "could not provide the constructor")
|
||||
}
|
||||
|
||||
4
go.mod
4
go.mod
@@ -8,7 +8,7 @@ require (
|
||||
github.com/golang/mock v1.2.0
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/int128/oauth2cli v1.1.0
|
||||
github.com/int128/oauth2cli v1.2.1
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/json-iterator/go v1.1.6 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
@@ -26,7 +26,7 @@ require (
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 // indirect
|
||||
k8s.io/client-go v10.0.0+incompatible
|
||||
|
||||
2
go.sum
2
go.sum
@@ -18,6 +18,8 @@ github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/int128/oauth2cli v1.1.0 h1:qAT6C8GyaLaSf0aseQUTcJvZ+j2MueETzGkpoFow0kc=
|
||||
github.com/int128/oauth2cli v1.1.0/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
|
||||
github.com/int128/oauth2cli v1.2.1 h1:rhYQ++Kijz/sleAfzy2u2qEsQJCQSHVYjANgOM/LfLA=
|
||||
github.com/int128/oauth2cli v1.2.1/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
|
||||
50
infrastructure/http.go
Normal file
50
infrastructure/http.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelDumpHeaders = 2
|
||||
logLevelDumpBody = 3
|
||||
)
|
||||
|
||||
type LoggingTransport struct {
|
||||
Base http.RoundTripper
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if !t.IsDumpEnabled() {
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
|
||||
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
|
||||
resp, err := t.Base.RoundTrip(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
|
||||
return resp, err
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) IsDumpEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpHeaders)
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) IsDumpBodyEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpBody)
|
||||
}
|
||||
90
infrastructure/http_test.go
Normal file
90
infrastructure/http_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
t.req = req
|
||||
return t.resp, nil
|
||||
}
|
||||
|
||||
func TestLoggingTransport_RoundTrip(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(gomock.Any()).
|
||||
Return(true).
|
||||
AnyTimes()
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
|
||||
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
|
||||
Host: example.com
|
||||
|
||||
dummy`)), req)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a response: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
transport := &LoggingTransport{
|
||||
Base: &mockTransport{resp: resp},
|
||||
Logger: logger,
|
||||
}
|
||||
gotResp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("RoundTrip error: %s", err)
|
||||
}
|
||||
if gotResp != resp {
|
||||
t.Errorf("resp wants %v but %v", resp, gotResp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
|
||||
Return(true)
|
||||
|
||||
transport := &LoggingTransport{
|
||||
Logger: logger,
|
||||
}
|
||||
if transport.IsDumpEnabled() != true {
|
||||
t.Errorf("IsDumpEnabled wants true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingTransport_IsDumpBodyEnabled(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(adaptors.LogLevel(logLevelDumpBody)).
|
||||
Return(true)
|
||||
|
||||
transport := &LoggingTransport{
|
||||
Logger: logger,
|
||||
}
|
||||
if transport.IsDumpBodyEnabled() != true {
|
||||
t.Errorf("IsDumpBodyEnabled wants true")
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,11 @@ func (c *OIDCAuthProvider) ExtraScopes() []string {
|
||||
return strings.Split(c.Config["extra-scopes"], ",")
|
||||
}
|
||||
|
||||
// IDToken returns the id-token.
|
||||
func (c *OIDCAuthProvider) IDToken() string {
|
||||
return c.Config["id-token"]
|
||||
}
|
||||
|
||||
// SetIDToken replaces the id-token.
|
||||
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
|
||||
c.Config["id-token"] = idToken
|
||||
|
||||
@@ -5,7 +5,7 @@ class Kubelogin < Formula
|
||||
version "{{ env "VERSION" }}"
|
||||
sha256 "{{ .darwin_amd64_zip_sha256 }}"
|
||||
def install
|
||||
bin.install "kubelogin_darwin_amd64" => "kubelogin"
|
||||
bin.install "kubelogin" => "kubelogin"
|
||||
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
|
||||
end
|
||||
test do
|
||||
|
||||
@@ -3,6 +3,7 @@ package usecases
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
@@ -30,15 +31,18 @@ type Login struct {
|
||||
}
|
||||
|
||||
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
u.Logger.Debugf(1, "WARNING: Log may contain your secrets, e.g. token or password")
|
||||
|
||||
u.Logger.Debugf(1, "Loading %s", in.KubeConfig)
|
||||
cfg, err := u.KubeConfig.LoadFromFile(in.KubeConfig)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read the kubeconfig")
|
||||
}
|
||||
|
||||
u.Logger.Logf("Using current-context: %s", cfg.CurrentContext)
|
||||
u.Logger.Printf("Using current-context: %s", cfg.CurrentContext)
|
||||
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
|
||||
if err != nil {
|
||||
u.Logger.Logf(oidcConfigErrorMessage, cfg.CurrentContext)
|
||||
u.Logger.Printf(oidcConfigErrorMessage, cfg.CurrentContext)
|
||||
return errors.Wrapf(err, "could not find an oidc auth-provider in the kubeconfig")
|
||||
}
|
||||
|
||||
@@ -46,16 +50,16 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
clientConfig.SetSkipTLSVerify(in.SkipTLSVerify)
|
||||
if authProvider.IDPCertificateAuthority() != "" {
|
||||
filename := authProvider.IDPCertificateAuthority()
|
||||
u.Logger.Logf("Using the certificate %s", filename)
|
||||
u.Logger.Printf("Using the certificate %s", filename)
|
||||
if err := clientConfig.AddCertificateFromFile(filename); err != nil {
|
||||
u.Logger.Logf("Skip the certificate %s: %s", filename, err)
|
||||
u.Logger.Printf("Skip the certificate %s: %s", filename, err)
|
||||
}
|
||||
}
|
||||
if authProvider.IDPCertificateAuthorityData() != "" {
|
||||
encoded := authProvider.IDPCertificateAuthorityData()
|
||||
u.Logger.Logf("Using certificate of idp-certificate-authority-data")
|
||||
u.Logger.Printf("Using the certificate of idp-certificate-authority-data")
|
||||
if err := clientConfig.AddEncodedCertificate(encoded); err != nil {
|
||||
u.Logger.Logf("Skip the certificate of idp-certificate-authority-data: %s", err)
|
||||
u.Logger.Printf("Skip the certificate of idp-certificate-authority-data: %s", err)
|
||||
}
|
||||
}
|
||||
hc, err := u.HTTP.NewClient(clientConfig)
|
||||
@@ -63,25 +67,67 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
return errors.Wrapf(err, "could not create a HTTP client")
|
||||
}
|
||||
|
||||
out, err := u.OIDC.Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: authProvider.IDPIssuerURL(),
|
||||
ClientID: authProvider.ClientID(),
|
||||
ClientSecret: authProvider.ClientSecret(),
|
||||
ExtraScopes: authProvider.ExtraScopes(),
|
||||
Client: hc,
|
||||
LocalServerPort: in.ListenPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
})
|
||||
if token := u.verifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
|
||||
IDToken: authProvider.IDToken(),
|
||||
Issuer: authProvider.IDPIssuerURL(),
|
||||
ClientID: authProvider.ClientID(),
|
||||
Client: hc,
|
||||
}); token != nil {
|
||||
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
|
||||
u.dumpIDToken(token)
|
||||
return nil
|
||||
}
|
||||
|
||||
out, err := u.OIDC.Authenticate(ctx,
|
||||
adaptors.OIDCAuthenticateIn{
|
||||
Issuer: authProvider.IDPIssuerURL(),
|
||||
ClientID: authProvider.ClientID(),
|
||||
ClientSecret: authProvider.ClientSecret(),
|
||||
ExtraScopes: authProvider.ExtraScopes(),
|
||||
Client: hc,
|
||||
LocalServerPort: in.ListenPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
},
|
||||
adaptors.OIDCAuthenticateCallback{
|
||||
ShowLocalServerURL: func(url string) {
|
||||
u.Logger.Printf("Open %s for authentication", url)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get token from OIDC provider")
|
||||
}
|
||||
|
||||
u.Logger.Logf("Got a token for subject=%s", out.VerifiedIDToken.Subject)
|
||||
u.Logger.Printf("You got a valid token until %s", out.VerifiedIDToken.Expiry)
|
||||
u.dumpIDToken(out.VerifiedIDToken)
|
||||
authProvider.SetIDToken(out.IDToken)
|
||||
authProvider.SetRefreshToken(out.RefreshToken)
|
||||
|
||||
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", in.KubeConfig)
|
||||
if err := u.KubeConfig.WriteToFile(cfg, in.KubeConfig); err != nil {
|
||||
return errors.Wrapf(err, "could not update the kubeconfig")
|
||||
}
|
||||
u.Logger.Logf("Updated %s", in.KubeConfig)
|
||||
u.Logger.Printf("Updated %s", in.KubeConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Login) dumpIDToken(token *oidc.IDToken) {
|
||||
var claims map[string]interface{}
|
||||
if err := token.Claims(&claims); err != nil {
|
||||
u.Logger.Debugf(1, "Error while inspection of the ID token: %s", err)
|
||||
}
|
||||
for k, v := range claims {
|
||||
u.Logger.Debugf(1, "The ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Login) verifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) *oidc.IDToken {
|
||||
if in.IDToken == "" {
|
||||
return nil
|
||||
}
|
||||
token, err := u.OIDC.VerifyIDToken(ctx, in)
|
||||
if err != nil {
|
||||
u.Logger.Debugf(1, "Could not verify the ID token in the kubeconfig: %s", err)
|
||||
return nil
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Do(func(_ context.Context, _ adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) {
|
||||
cb.ShowLocalServerURL("http://localhost:10000")
|
||||
}).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
@@ -102,7 +105,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -133,7 +136,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -144,7 +147,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -177,7 +180,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
SkipOpenBrowser: true,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -188,7 +191,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -199,6 +202,98 @@ func TestLogin_Do(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/ValidToken", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "VALID"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
kubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/kubeconfig").
|
||||
Return(inConfig, nil)
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
|
||||
IDToken: "VALID",
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
Client: httpClient,
|
||||
}).
|
||||
Return(&oidc.IDToken{}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: kubeConfig,
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/InvalidToken", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "EXPIRED"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
|
||||
IDToken: "EXPIRED",
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
Client: httpClient,
|
||||
}).
|
||||
Return(nil, errors.New("token is expired"))
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/extra-scopes", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["extra-scopes"] = "email,profile"
|
||||
@@ -221,7 +316,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -232,7 +327,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -266,7 +361,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -277,7 +372,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -312,7 +407,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -323,7 +418,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -357,7 +452,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -368,7 +463,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
@@ -403,7 +498,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}).
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
@@ -414,7 +509,7 @@ func TestLogin_Do(t *testing.T) {
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: t,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
|
||||
Reference in New Issue
Block a user