Compare commits

...

13 Commits

Author SHA1 Message Date
Hidetake Iwata
97b0bdd53c Fix build error of windows_amd64 binary (#97) 2019-06-06 09:30:41 +09:00
Hidetake Iwata
ebe5feaed2 Transparently login feature (#95)
* Introduce spf13/cobra

* Add login transparently feature
2019-06-05 22:01:53 +09:00
flarno11
4427bc7985 fixed typo in kubectl plugin call (#94) 2019-06-04 08:15:00 +09:00
Hidetake Iwata
8643d695fe Refactor (#92)
* Refactor test

* Refactor: rename and add comments for ShowLocalServerURL
2019-06-03 20:01:51 +09:00
Hidetake Iwata
f0cff5a54b Add password prompt (#91) 2019-06-01 19:57:11 +09:00
Hidetake Iwata
8237928af3 Refactor (#90)
* Refactor: move HTTP adaptor to internal

* Refactor: extract models/kubeconfig package

* Refactor: rename to Kubeconfig from KubeConfig

* Refactor: simplify use-case
2019-05-29 15:31:52 +09:00
Hidetake Iwata
01b270755b Refactor: use a dynamic port in the integration tests (#89)
* Refactor: use dynamic port for auth server in integration tests

* Refactor: use dynamic port for local server in integration tests
2019-05-28 09:05:15 +09:00
Hidetake Iwata
e97b4de40b Refactor packages structure (#88)
* Refactor: move usecases implementation to dedicated packages

* Refactor: move adaptors implementation to dedicated packages

* Refactor: rename and move packages

* Refactor: split to OIDC and OIDCClient
2019-05-24 11:41:09 +09:00
Hidetake Iwata
5063550468 Add resource owner password credentials grant support (#87) 2019-05-23 09:37:47 +09:00
Hidetake Iwata
d5989ca256 Move to google/wire (#86) 2019-05-20 10:55:02 +09:00
Hidetake Iwata
c508a1b717 Refactor docs (#84) 2019-05-19 16:07:57 +09:00
Hidetake Iwata
e133ea8541 Add codecov (#83) 2019-05-18 09:45:37 +09:00
Hidetake Iwata
3f2e84a1ea Update README.md 2019-05-17 09:24:08 +09:00
57 changed files with 2886 additions and 1859 deletions

View File

@@ -21,6 +21,7 @@ jobs:
# workaround for https://github.com/golang/go/issues/27925
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make run
- run: |
if [ "$CIRCLE_TAG" ]; then

6
.gitignore vendored
View File

@@ -1,5 +1,9 @@
/.idea
/.kubeconfig*
/dist
/coverage.out
/kubelogin
/kubectl-oidc_login
/.kubeconfig*

View File

@@ -10,7 +10,7 @@ all: $(TARGET)
check:
golangci-lint run
$(MAKE) -C adaptors_test/keys/testdata
go test -v -race ./...
go test -v -race -cover -coverprofile=coverage.out ./...
$(TARGET): $(wildcard *.go)
go build -o $@ -ldflags "$(LDFLAGS)"

203
README.md
View File

@@ -1,18 +1,11 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin)
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
It updates the kubeconfig file with an ID token and refresh token got from the OIDC provider.
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
It gets a token from the OIDC provider and writes it to the kubeconfig.
## Getting Started
You need to setup the following components:
- OIDC provider
- Kubernetes API server
- Role for your group or user
- kubectl authentication
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
@@ -29,23 +22,12 @@ unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
After initial setup or when the token has been expired, just run:
You need to setup the following components:
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
```
or run as a kubectl plugin:
```
% kubectl oidc-login
```
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
- OIDC provider
- Kubernetes API server
- kubectl authentication
- Role binding
For more, see the following documents:
@@ -53,7 +35,81 @@ For more, see the following documents:
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
If you are using other platforms, please contribute documents via pull requests.
### Login manually
Just run:
```sh
kubelogin
# or run as a kubectl plugin
kubectl oidc-login
```
It automatically opens the browser and you can log in to the provider.
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
After authentication, it writes an ID token and refresh token to the kubeconfig.
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
Updated ~/.kubeconfig
```
Now you can access to the cluster.
```
% kubectl get pods
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the token is valid, kubelogin does nothing.
```
% kubelogin
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
```
### Login transparently
You can wrap kubectl to transparently login to the provider.
```sh
alias kubectl='kubelogin exec -- kubectl'
# or run as a kubectl plugin
alias kubectl='kubectl oidc-login exec -- kubectl'
```
If the token expired, it updates the kubeconfig and executes kubectl.
```
% kubectl get pods
Open http://localhost:8000 for authentication
You got a valid token until 2019-06-05 19:05:34 +0900 JST
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
If the token is valid, it just executes kubectl.
```
% kubectl get pods
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
It respects kubectl options passed to the extra arguments.
For example, if you run `kubectl --kubeconfig .kubeconfig`,
it will update `.kubeconfig` and execute kubectl.
If the current auth provider is not `oidc`, it just executes kubectl.
## Configuration
@@ -61,18 +117,40 @@ If you are using other platforms, please contribute documents via pull requests.
This document is for the development version.
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
Kubelogin supports the following options.
Kubelogin supports the following options:
```
Options:
Usage:
kubelogin [flags]
kubelogin [command]
Examples:
# Login to the provider using authorization code grant.
kubelogin
# Login to the provider using resource owner password credentials grant.
kubelogin --username USERNAME --password PASSWORD
# Wrap kubectl and login transparently
alias kubectl='kubelogin exec -- kubectl'
Available Commands:
exec Login transparently and execute the kubectl command
help Help about any command
version Print the version information
Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-v, --v int If set to 1 or greater, it shows debug log
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
-h, --help help for kubelogin
```
It supports the following keys of `auth-provider` in a kubeconfig.
@@ -106,6 +184,54 @@ KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
### Authentication flows
#### Authorization code flow
Kubelogin performs the authorization code flow by default.
It starts the local server at port 8000 or 18000 by default.
You need to register the following redirect URIs to the OIDC provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the ports by the option:
```sh
kubelogin --listen-port 12345 --listen-port 23456
```
#### Resource owner password credentials grant flow
You can use the resource owner password credentials grant flow.
Note that not all providers support this flow as:
> The resource owner password credentials grant type is suitable in
> cases where the resource owner has a trust relationship with the
> client, such as the device operating system or a highly privileged
> application.
> The authorization server should take special care when
> enabling this grant type and only allow it when other flows are not
> viable.
>
> <https://tools.ietf.org/html/rfc6749#section-4.3>
You can pass the username and password:
```
% kubelogin --username USER --password PASS
```
or ask the password:
```
% kubelogin --username USER
Password:
```
### Extra scopes
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
@@ -122,21 +248,6 @@ sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
### Redirect URIs
By default kubelogin starts the local server at port 8000 or 18000.
You need to register the following redirect URIs to the OIDC provider:
- `http://localhost:8000`
- `http://localhost:18000` (used if port 8000 is already in use)
You can change the ports by the option:
```sh
kubelogin --listen-port 12345 --listen-port 23456
```
### CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by kubeconfig or option.

79
adaptors/adaptors.go Normal file
View File

@@ -0,0 +1,79 @@
package adaptors
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/models/kubeconfig"
)
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,OIDC,OIDCClient,Env,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type Kubeconfig interface {
GetCurrentAuth(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error)
UpdateAuth(auth *kubeconfig.Auth) error
}
type OIDC interface {
New(config OIDCClientConfig) (OIDCClient, error)
}
type OIDCClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
SkipTLSVerify bool
}
type OIDCClient interface {
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*oidc.IDToken, error)
}
type OIDCAuthenticateByCodeIn struct {
Config kubeconfig.OIDCConfig
LocalServerPort []int // HTTP server port candidates
SkipOpenBrowser bool // skip opening browser if true
ShowLocalServerURL interface{ ShowLocalServerURL(url string) }
}
type OIDCAuthenticateByPasswordIn struct {
Config kubeconfig.OIDCConfig
Username string
Password string
}
type OIDCAuthenticateOut struct {
VerifiedIDToken *oidc.IDToken
IDToken string
RefreshToken string
}
type OIDCVerifyIn struct {
Config kubeconfig.OIDCConfig
}
type Env interface {
ReadPassword(prompt string) (string, error)
Exec(ctx context.Context, executable string, args []string) (int, error)
}
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

View File

@@ -1,100 +0,0 @@
package adaptors
import (
"context"
"strings"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/spf13/pflag"
"go.uber.org/dig"
)
const usage = `Login to the OpenID Connect provider and update the kubeconfig.
kubelogin %[2]s
Examples:
# Login to the current provider and update ~/.kube/config
%[1]s
Options:
%[3]s
Usage:
%[1]s [options]`
var defaultListenPort = []int{8000, 18000}
func NewCmd(i Cmd) adaptors.Cmd {
return &i
}
type Cmd struct {
dig.In
Login usecases.Login
Logger adaptors.Logger
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
executable := executableName(args[0])
f := pflag.NewFlagSet(executable, pflag.ContinueOnError)
f.SortFlags = false
f.Usage = func() {
cmd.Logger.Printf(usage, executable, version, f.FlagUsages())
}
var o cmdOptions
f.StringVar(&o.KubeConfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.KubeContext, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.KubeUser, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
if err := f.Parse(args[1:]); err != nil {
if err == pflag.ErrHelp {
return 1
}
cmd.Logger.Printf("Error: invalid arguments: %s", err)
return 1
}
if len(f.Args()) > 0 {
cmd.Logger.Printf("Error: too many arguments")
return 1
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginIn{
KubeConfigFilename: o.KubeConfig,
KubeContextName: kubeconfig.ContextName(o.KubeContext),
KubeUserName: kubeconfig.UserName(o.KubeUser),
CertificateAuthorityFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("Error: %s", err)
return 1
}
return 0
}
type cmdOptions struct {
KubeConfig string
KubeContext string
KubeUser string
CertificateAuthority string
SkipTLSVerify bool
ListenPort []int
SkipOpenBrowser bool
Verbose int
}
func executableName(arg0 string) string {
if strings.HasPrefix(arg0, "kubectl-") {
return strings.ReplaceAll(strings.ReplaceAll(arg0, "-", " "), "_", "-")
}
return arg0
}

168
adaptors/cmd/cmd.go Normal file
View File

@@ -0,0 +1,168 @@
package cmd
import (
"context"
"fmt"
"path/filepath"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const examples = ` # Login to the provider using authorization code grant.
%[1]s
# Login to the provider using resource owner password credentials grant.
%[1]s --username USERNAME --password PASSWORD
# Wrap kubectl and login transparently
alias kubectl='%[1]s exec -- kubectl'`
var defaultListenPort = []int{8000, 18000}
type Cmd struct {
Login usecases.Login
LoginAndExec usecases.LoginAndExec
Logger adaptors.Logger
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var exitCode int
executable := filepath.Base(args[0])
var o struct {
kubectlOptions
kubeloginOptions
}
rootCmd := cobra.Command{
Use: executable,
Short: "Login to the OpenID Connect provider and update the kubeconfig",
Example: fmt.Sprintf(examples, executable),
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginIn{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("error: %s", err)
exitCode = 1
return
}
},
}
o.kubectlOptions.register(rootCmd.Flags())
o.kubeloginOptions.register(rootCmd.Flags())
execCmd := cobra.Command{
Use: "exec [flags] -- kubectl [args]",
Short: "Login transparently and execute the kubectl command",
Args: func(execCmd *cobra.Command, args []string) error {
if execCmd.ArgsLenAtDash() == -1 {
return errors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
}
if len(args) < 1 {
return errors.New("too few arguments")
}
return nil
},
Run: func(execCmd *cobra.Command, args []string) {
// parse the extra args and override the kubectl options
f := pflag.NewFlagSet(execCmd.Name(), pflag.ContinueOnError)
o.kubectlOptions.register(f)
// ignore unknown flags and help flags (-h/--help)
f.ParseErrorsWhitelist.UnknownFlags = true
f.BoolP("help", "h", false, "ignore help flags")
if err := f.Parse(args); err != nil {
cmd.Logger.Debugf(1, "error while parsing the extra arguments: %s", err)
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
in := usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
ListenPort: o.ListenPort,
SkipOpenBrowser: o.SkipOpenBrowser,
Username: o.Username,
Password: o.Password,
},
Executable: args[0],
Args: args[1:],
}
out, err := cmd.LoginAndExec.Do(ctx, in)
if err != nil {
cmd.Logger.Printf("error: %s", err)
exitCode = 1
return
}
exitCode = out.ExitCode
},
}
o.kubeloginOptions.register(execCmd.Flags())
rootCmd.AddCommand(&execCmd)
versionCmd := cobra.Command{
Use: "version",
Short: "Print the version information",
Args: cobra.NoArgs,
Run: func(*cobra.Command, []string) {
cmd.Logger.Printf("%s version %s", executable, version)
},
}
rootCmd.AddCommand(&versionCmd)
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
cmd.Logger.Debugf(1, "error while parsing the arguments: %s", err)
return 1
}
return exitCode
}
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
}
func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
}
type kubeloginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
func (o *kubeloginOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}

268
adaptors/cmd/cmd_test.go Normal file
View File

@@ -0,0 +1,268 @@
package cmd
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/mock_usecases"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("login/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
ListenPort: defaultListenPort,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("login/TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("loginAndExec/Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: defaultListenPort,
},
Executable: "kubectl",
Args: []string{"dummy"},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable, "exec", "--", "kubectl", "dummy"}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("loginAndExec/OptionsInExtraArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig2",
KubeconfigContext: "hello2.k8s.local",
KubeconfigUser: "google2",
CACertFilename: "/path/to/cacert2",
SkipTLSVerify: true,
ListenPort: defaultListenPort,
},
Executable: "kubectl",
Args: []string{
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
"--help",
},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"exec",
"--",
"kubectl",
// kubectl options in the extra args should be mapped to the options
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
// kubelogin options in the extra args should not affect
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
"--help",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("loginAndExec/OverrideOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
loginAndExec.EXPECT().
Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig2",
KubeconfigContext: "hello2.k8s.local",
KubeconfigUser: "google2",
CACertFilename: "/path/to/cacert2",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
Username: "USER",
Password: "PASS",
},
Executable: "kubectl",
Args: []string{
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
},
}).
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
cmd := Cmd{
LoginAndExec: loginAndExec,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
// kubelogin options in the first args should be mapped to the options
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
"exec",
"--",
"kubectl",
// kubectl options in the extra args should be mapped to the options
"--kubeconfig", "/path/to/kubeconfig2",
"--context", "hello2.k8s.local",
"--user", "google2",
"--certificate-authority", "/path/to/cacert2",
"--insecure-skip-tls-verify",
"-v2",
// kubelogin options in the extra args should not affect
"--listen-port", "30080",
"--skip-open-browser",
"--username", "USER2",
"--password", "PASS2",
"dummy",
"--dummy",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}

View File

@@ -1,111 +0,0 @@
package adaptors
import (
"context"
"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"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
ListenPort: defaultListenPort,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeConfigFilename: "/path/to/kubeconfig",
KubeContextName: "hello.k8s.local",
KubeUserName: "google",
CertificateAuthorityFilename: "/path/to/cacert",
SkipTLSVerify: true,
ListenPort: []int{10080, 20080},
SkipOpenBrowser: true,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--listen-port", "10080",
"--listen-port", "20080",
"--skip-open-browser",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
}
func TestCmd_executableName(t *testing.T) {
t.Run("kubelogin", func(t *testing.T) {
e := executableName("kubelogin")
if e != "kubelogin" {
t.Errorf("executableName wants kubelogin but %s", e)
}
})
t.Run("kubectl-oidc_login", func(t *testing.T) {
e := executableName("kubectl-oidc_login")
if e != "kubectl oidc-login" {
t.Errorf("executableName wants kubectl oidc-login but %s", e)
}
})
}

44
adaptors/env/env.go vendored Normal file
View File

@@ -0,0 +1,44 @@
package env
import (
"context"
"fmt"
"os"
"os/exec"
"syscall"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh/terminal"
)
// Env provides environment specific facilities.
type Env struct{}
// ReadPassword reads a password from the stdin without echo back.
func (*Env) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, "Password: "); err != nil {
return "", errors.Wrapf(err, "could not write the prompt")
}
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", errors.Wrapf(err, "could not read")
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", errors.Wrapf(err, "could not write a new line")
}
return string(b), nil
}
func (*Env) Exec(ctx context.Context, executable string, args []string) (int, error) {
c := exec.CommandContext(ctx, executable, args...)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
if err, ok := err.(*exec.ExitError); ok {
return err.ExitCode(), nil
}
return 0, errors.Wrapf(err, "could not execute the command")
}
return 0, nil
}

View File

@@ -1,74 +0,0 @@
package adaptors
import (
"context"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/kubeconfig"
)
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,OIDC,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type KubeConfig interface {
LoadByDefaultRules(filename string) (*kubeconfig.Config, error)
LoadFromFile(filename string) (*kubeconfig.Config, error)
WriteToFile(config *kubeconfig.Config, filename string) error
}
type HTTP interface {
NewClient(config HTTPClientConfig) (*http.Client, error)
}
type HTTPClientConfig struct {
OIDCConfig kubeconfig.OIDCConfig
CertificateAuthorityFilename string
SkipTLSVerify bool
}
type OIDC interface {
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
Verify(ctx context.Context, in OIDCVerifyIn) (*oidc.IDToken, error)
}
type OIDCAuthenticateIn struct {
Config kubeconfig.OIDCConfig
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort []int // HTTP server port candidates
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 OIDCVerifyIn struct {
Config kubeconfig.OIDCConfig
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

View File

@@ -1,44 +0,0 @@
package adaptors
import (
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/kubeconfig"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func NewKubeConfig() adaptors.KubeConfig {
return &KubeConfig{}
}
type KubeConfig struct{}
// LoadByDefaultRules loads the config by the default rules, that is same as kubectl.
func (*KubeConfig) LoadByDefaultRules(filename string) (*kubeconfig.Config, error) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = filename
config, err := rules.Load()
if err != nil {
return nil, errors.Wrapf(err, "could not read the kubeconfig")
}
return (*kubeconfig.Config)(config), err
}
// LoadFromFile loads the config from the single file.
func (*KubeConfig) LoadFromFile(filename string) (*kubeconfig.Config, error) {
config, err := clientcmd.LoadFromFile(filename)
if err != nil {
return nil, errors.Wrapf(err, "could not read the kubeconfig from %s", filename)
}
return (*kubeconfig.Config)(config), err
}
// WriteToFile writes the config to the single file.
func (*KubeConfig) WriteToFile(config *kubeconfig.Config, filename string) error {
err := clientcmd.WriteToFile(*(*api.Config)(config), filename)
if err != nil {
return errors.Wrapf(err, "could not write the kubeconfig to %s", filename)
}
return err
}

View File

@@ -0,0 +1,3 @@
package kubeconfig
type Kubeconfig struct{}

View File

@@ -0,0 +1,85 @@
package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func (*Kubeconfig) GetCurrentAuth(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, errors.WithStack(err)
}
auth, err := findCurrentAuth(config, contextName, userName)
if err != nil {
return nil, errors.Wrapf(err, "could not find the current auth provider")
}
return auth, nil
}
func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, errors.Wrapf(err, "could not load the kubeconfig")
}
return config, err
}
// findCurrentAuth resolves the current auth provider.
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func findCurrentAuth(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.Auth, error) {
if userName == "" {
if contextName == "" {
contextName = kubeconfig.ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, errors.Errorf("context %s does not exist", contextName)
}
userName = kubeconfig.UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
return nil, errors.Errorf("user %s does not exist", userName)
}
if userNode.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
}
if userNode.AuthProvider.Config == nil {
return nil, errors.Errorf("auth-provider.config is missing")
}
return &kubeconfig.Auth{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
OIDCConfig: makeOIDCConfig(userNode.AuthProvider.Config),
}, nil
}
func makeOIDCConfig(m map[string]string) kubeconfig.OIDCConfig {
var extraScopes []string
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return kubeconfig.OIDCConfig{
IDPIssuerURL: m["idp-issuer-url"],
ClientID: m["client-id"],
ClientSecret: m["client-secret"],
IDPCertificateAuthority: m["idp-certificate-authority"],
IDPCertificateAuthorityData: m["idp-certificate-authority-data"],
ExtraScopes: extraScopes,
IDToken: m["id-token"],
RefreshToken: m["refresh-token"],
}
}

View File

@@ -0,0 +1,225 @@
package kubeconfig
import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/models/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
func Test_loadByDefaultRules(t *testing.T) {
t.Run("google.yaml>keycloak.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := loadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "google@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
t.Run("keycloak.yaml>google.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := loadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "keycloak@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}
func Test_findCurrentAuth(t *testing.T) {
t.Run("CurrentContext", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
CurrentContext: "theContext",
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
},
},
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"idp-issuer-url": "https://accounts.google.com",
"client-id": "GOOGLE_CLIENT_ID",
"client-secret": "GOOGLE_CLIENT_SECRET",
"idp-certificate-authority": "/path/to/cert",
"idp-certificate-authority-data": "BASE64",
"extra-scopes": "email,profile",
"id-token": "YOUR_ID_TOKEN",
"refresh-token": "YOUR_REFRESH_TOKEN",
},
},
},
},
}, "", "")
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
}
})
t.Run("ByContextName", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
Contexts: map[string]*api.Context{
"theContext": {
AuthInfo: "theUser",
},
},
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"idp-issuer-url": "https://accounts.google.com",
},
},
},
},
}, "theContext", "")
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
}
})
t.Run("ByUserName", func(t *testing.T) {
auth, err := findCurrentAuth(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"idp-issuer-url": "https://accounts.google.com",
},
},
},
},
}, "", "theUser")
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.Auth{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
}
if diff := deep.Equal(want, auth); diff != nil {
t.Error(diff)
}
})
t.Run("NoConfig", func(t *testing.T) {
_, err := findCurrentAuth(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
},
},
},
}, "", "theUser")
if err == nil {
t.Fatalf("wants error but nil")
}
})
t.Run("NotOIDC", func(t *testing.T) {
_, err := findCurrentAuth(&api.Config{
AuthInfos: map[string]*api.AuthInfo{
"theUser": {
LocationOfOrigin: "/path/to/kubeconfig",
AuthProvider: &api.AuthProviderConfig{
Name: "some",
Config: map[string]string{"foo": "bar"},
},
},
},
}, "", "theUser")
if err == nil {
t.Fatalf("wants error but nil")
}
})
}

View File

@@ -0,0 +1,51 @@
package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuth(auth *kubeconfig.Auth) error {
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
if err != nil {
return errors.Wrapf(err, "could not load %s", auth.LocationOfOrigin)
}
userNode, ok := config.AuthInfos[string(auth.UserName)]
if !ok {
return errors.Errorf("user %s does not exist", auth.UserName)
}
if userNode.AuthProvider == nil {
return errors.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return errors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
}
copyOIDCConfig(auth.OIDCConfig, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, auth.LocationOfOrigin); err != nil {
return errors.Wrapf(err, "could not update %s", auth.LocationOfOrigin)
}
return nil
}
func copyOIDCConfig(config kubeconfig.OIDCConfig, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", config.IDPIssuerURL)
setOrDeleteKey(m, "client-id", config.ClientID)
setOrDeleteKey(m, "client-secret", config.ClientSecret)
setOrDeleteKey(m, "idp-certificate-authority", config.IDPCertificateAuthority)
setOrDeleteKey(m, "idp-certificate-authority-data", config.IDPCertificateAuthorityData)
extraScopes := strings.Join(config.ExtraScopes, ",")
setOrDeleteKey(m, "extra-scopes", extraScopes)
setOrDeleteKey(m, "id-token", config.IDToken)
setOrDeleteKey(m, "refresh-token", config.RefreshToken)
}
func setOrDeleteKey(m map[string]string, key, value string) {
if value == "" {
delete(m, key)
return
}
m[key] = value
}

View File

@@ -0,0 +1,138 @@
package kubeconfig
import (
"io/ioutil"
"os"
"testing"
"github.com/int128/kubelogin/models/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
var k Kubeconfig
t.Run("MinimumKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
defer func() {
if err := os.Remove(f.Name()); err != nil {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuth(&kubeconfig.Auth{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
b, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatalf("Could not read kubeconfig: %s", err)
}
want := `apiVersion: v1
clusters: []
contexts: []
current-context: ""
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
client-id: GOOGLE_CLIENT_ID
client-secret: GOOGLE_CLIENT_SECRET
id-token: YOUR_ID_TOKEN
idp-issuer-url: https://accounts.google.com
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
}
})
t.Run("FullKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
defer func() {
if err := os.Remove(f.Name()); err != nil {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuth(&kubeconfig.Auth{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
IDPCertificateAuthority: "/path/to/cert",
IDPCertificateAuthorityData: "BASE64",
ExtraScopes: []string{"email", "profile"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
},
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
b, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatalf("Could not read kubeconfig: %s", err)
}
want := `apiVersion: v1
clusters: []
contexts: []
current-context: ""
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
client-id: GOOGLE_CLIENT_ID
client-secret: GOOGLE_CLIENT_SECRET
extra-scopes: email,profile
id-token: YOUR_ID_TOKEN
idp-certificate-authority: /path/to/cert
idp-certificate-authority-data: BASE64
idp-issuer-url: https://accounts.google.com
refresh-token: YOUR_REFRESH_TOKEN
name: oidc
`
if want != string(b) {
t.Errorf("---- kubeconfig wants ----\n%s\n---- but ----\n%s", want, string(b))
}
})
}
func newKubeconfigFile(t *testing.T) *os.File {
content := `apiVersion: v1
clusters: []
kind: Config
preferences: {}
users:
- name: google
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
name: oidc`
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatalf("Could not create a file: %s", err)
}
defer f.Close()
if _, err := f.Write([]byte(content)); err != nil {
t.Fatalf("Could not write kubeconfig: %s", err)
}
return f
}

View File

@@ -1,74 +0,0 @@
package adaptors
import (
"os"
"testing"
)
func TestKubeConfig_LoadByDefaultRules(t *testing.T) {
var adaptor KubeConfig
t.Run("google.yaml>keycloak.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := adaptor.LoadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "google@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
t.Run("keycloak.yaml>google.yaml", func(t *testing.T) {
setenv(t, "KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := adaptor.LoadByDefaultRules("")
if err != nil {
t.Fatalf("Could not load the configs: %s", err)
}
if w := "keycloak@hello.k8s.local"; w != config.CurrentContext {
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
}
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
t.Errorf("Contexts[google@hello.k8s.local] is missing")
}
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
}
if _, ok := config.AuthInfos["google"]; !ok {
t.Errorf("AuthInfos[google] is missing")
}
if _, ok := config.AuthInfos["keycloak"]; !ok {
t.Errorf("AuthInfos[keycloak] is missing")
}
})
}
func setenv(t *testing.T, key, value string) {
t.Helper()
if err := os.Setenv(key, value); err != nil {
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
}
}
func unsetenv(t *testing.T, key string) {
t.Helper()
if err := os.Unsetenv(key); err != nil {
t.Fatalf("Could not unset the env var %s: %s", key, err)
}
}

View File

@@ -1,22 +1,22 @@
package adaptors
package logger
import (
"log"
"os"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors"
)
// NewLogger returns a Logger with the standard log.Logger for messages and debug.
func NewLogger() adaptors.Logger {
// New returns a Logger with the standard log.Logger for messages and debug.
func New() adaptors.Logger {
return &Logger{
stdLogger: log.New(os.Stderr, "", 0),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
}
}
// NewLoggerWith returns a Logger with the given standard log.Logger.
func NewLoggerWith(l stdLogger) *Logger {
// FromStdLogger returns a Logger with the given standard log.Logger.
func FromStdLogger(l stdLogger) *Logger {
return &Logger{
stdLogger: l,
debugLogger: l,

View File

@@ -1,10 +1,10 @@
package adaptors
package logger
import (
"fmt"
"testing"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors"
)
type mockStdLogger struct {

View File

@@ -2,7 +2,7 @@ package mock_adaptors
import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors"
)
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,OIDC,Logger)
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,OIDC,OIDCClient,Env,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
@@ -8,107 +8,57 @@ import (
context "context"
go_oidc "github.com/coreos/go-oidc"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
kubeconfig "github.com/int128/kubelogin/kubeconfig"
http "net/http"
adaptors "github.com/int128/kubelogin/adaptors"
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
reflect "reflect"
)
// MockKubeConfig is a mock of KubeConfig interface
type MockKubeConfig struct {
// MockKubeconfig is a mock of Kubeconfig interface
type MockKubeconfig struct {
ctrl *gomock.Controller
recorder *MockKubeConfigMockRecorder
recorder *MockKubeconfigMockRecorder
}
// MockKubeConfigMockRecorder is the mock recorder for MockKubeConfig
type MockKubeConfigMockRecorder struct {
mock *MockKubeConfig
// MockKubeconfigMockRecorder is the mock recorder for MockKubeconfig
type MockKubeconfigMockRecorder struct {
mock *MockKubeconfig
}
// NewMockKubeConfig creates a new mock instance
func NewMockKubeConfig(ctrl *gomock.Controller) *MockKubeConfig {
mock := &MockKubeConfig{ctrl: ctrl}
mock.recorder = &MockKubeConfigMockRecorder{mock}
// NewMockKubeconfig creates a new mock instance
func NewMockKubeconfig(ctrl *gomock.Controller) *MockKubeconfig {
mock := &MockKubeconfig{ctrl: ctrl}
mock.recorder = &MockKubeconfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockKubeConfig) EXPECT() *MockKubeConfigMockRecorder {
func (m *MockKubeconfig) EXPECT() *MockKubeconfigMockRecorder {
return m.recorder
}
// LoadByDefaultRules mocks base method
func (m *MockKubeConfig) LoadByDefaultRules(arg0 string) (*kubeconfig.Config, error) {
ret := m.ctrl.Call(m, "LoadByDefaultRules", arg0)
ret0, _ := ret[0].(*kubeconfig.Config)
// GetCurrentAuth mocks base method
func (m *MockKubeconfig) GetCurrentAuth(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.Auth, error) {
ret := m.ctrl.Call(m, "GetCurrentAuth", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.Auth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadByDefaultRules indicates an expected call of LoadByDefaultRules
func (mr *MockKubeConfigMockRecorder) LoadByDefaultRules(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadByDefaultRules", reflect.TypeOf((*MockKubeConfig)(nil).LoadByDefaultRules), arg0)
// GetCurrentAuth indicates an expected call of GetCurrentAuth
func (mr *MockKubeconfigMockRecorder) GetCurrentAuth(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuth", reflect.TypeOf((*MockKubeconfig)(nil).GetCurrentAuth), arg0, arg1, arg2)
}
// LoadFromFile mocks base method
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*kubeconfig.Config, error) {
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
ret0, _ := ret[0].(*kubeconfig.Config)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadFromFile indicates an expected call of LoadFromFile
func (mr *MockKubeConfigMockRecorder) LoadFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadFromFile", reflect.TypeOf((*MockKubeConfig)(nil).LoadFromFile), arg0)
}
// WriteToFile mocks base method
func (m *MockKubeConfig) WriteToFile(arg0 *kubeconfig.Config, arg1 string) error {
ret := m.ctrl.Call(m, "WriteToFile", arg0, arg1)
// UpdateAuth mocks base method
func (m *MockKubeconfig) UpdateAuth(arg0 *kubeconfig.Auth) error {
ret := m.ctrl.Call(m, "UpdateAuth", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// WriteToFile indicates an expected call of WriteToFile
func (mr *MockKubeConfigMockRecorder) WriteToFile(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteToFile", reflect.TypeOf((*MockKubeConfig)(nil).WriteToFile), arg0, arg1)
}
// MockHTTP is a mock of HTTP interface
type MockHTTP struct {
ctrl *gomock.Controller
recorder *MockHTTPMockRecorder
}
// MockHTTPMockRecorder is the mock recorder for MockHTTP
type MockHTTPMockRecorder struct {
mock *MockHTTP
}
// NewMockHTTP creates a new mock instance
func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP {
mock := &MockHTTP{ctrl: ctrl}
mock.recorder = &MockHTTPMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder {
return m.recorder
}
// NewClient mocks base method
func (m *MockHTTP) NewClient(arg0 interfaces.HTTPClientConfig) (*http.Client, error) {
ret := m.ctrl.Call(m, "NewClient", arg0)
ret0, _ := ret[0].(*http.Client)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewClient indicates an expected call of NewClient
func (mr *MockHTTPMockRecorder) NewClient(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockHTTP)(nil).NewClient), arg0)
// UpdateAuth indicates an expected call of UpdateAuth
func (mr *MockKubeconfigMockRecorder) UpdateAuth(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuth", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuth), arg0)
}
// MockOIDC is a mock of OIDC interface
@@ -134,21 +84,70 @@ func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
return m.recorder
}
// Authenticate mocks base method
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)
// New mocks base method
func (m *MockOIDC) New(arg0 adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
ret := m.ctrl.Call(m, "New", arg0)
ret0, _ := ret[0].(adaptors.OIDCClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authenticate indicates an expected call of Authenticate
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)
// New indicates an expected call of New
func (mr *MockOIDCMockRecorder) New(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOIDC)(nil).New), arg0)
}
// MockOIDCClient is a mock of OIDCClient interface
type MockOIDCClient struct {
ctrl *gomock.Controller
recorder *MockOIDCClientMockRecorder
}
// MockOIDCClientMockRecorder is the mock recorder for MockOIDCClient
type MockOIDCClientMockRecorder struct {
mock *MockOIDCClient
}
// NewMockOIDCClient creates a new mock instance
func NewMockOIDCClient(ctrl *gomock.Controller) *MockOIDCClient {
mock := &MockOIDCClient{ctrl: ctrl}
mock.recorder = &MockOIDCClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDCClient) EXPECT() *MockOIDCClientMockRecorder {
return m.recorder
}
// AuthenticateByCode mocks base method
func (m *MockOIDCClient) AuthenticateByCode(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByCode indicates an expected call of AuthenticateByCode
func (mr *MockOIDCClientMockRecorder) AuthenticateByCode(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByCode), arg0, arg1)
}
// AuthenticateByPassword mocks base method
func (m *MockOIDCClient) AuthenticateByPassword(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
func (mr *MockOIDCClientMockRecorder) AuthenticateByPassword(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByPassword), arg0, arg1)
}
// Verify mocks base method
func (m *MockOIDC) Verify(arg0 context.Context, arg1 interfaces.OIDCVerifyIn) (*go_oidc.IDToken, error) {
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*go_oidc.IDToken, error) {
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
ret0, _ := ret[0].(*go_oidc.IDToken)
ret1, _ := ret[1].(error)
@@ -156,8 +155,57 @@ func (m *MockOIDC) Verify(arg0 context.Context, arg1 interfaces.OIDCVerifyIn) (*
}
// Verify indicates an expected call of Verify
func (mr *MockOIDCMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDC)(nil).Verify), arg0, arg1)
func (mr *MockOIDCClientMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDCClient)(nil).Verify), arg0, arg1)
}
// MockEnv is a mock of Env interface
type MockEnv struct {
ctrl *gomock.Controller
recorder *MockEnvMockRecorder
}
// MockEnvMockRecorder is the mock recorder for MockEnv
type MockEnvMockRecorder struct {
mock *MockEnv
}
// NewMockEnv creates a new mock instance
func NewMockEnv(ctrl *gomock.Controller) *MockEnv {
mock := &MockEnv{ctrl: ctrl}
mock.recorder = &MockEnvMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockEnv) EXPECT() *MockEnvMockRecorder {
return m.recorder
}
// Exec mocks base method
func (m *MockEnv) Exec(arg0 context.Context, arg1 string, arg2 []string) (int, error) {
ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec
func (mr *MockEnvMockRecorder) Exec(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockEnv)(nil).Exec), arg0, arg1, arg2)
}
// ReadPassword mocks base method
func (m *MockEnv) ReadPassword(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockEnvMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockEnv)(nil).ReadPassword), arg0)
}
// MockLogger is a mock of Logger interface
@@ -184,7 +232,7 @@ func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
}
// Debugf mocks base method
func (m *MockLogger) Debugf(arg0 interfaces.LogLevel, arg1 string, arg2 ...interface{}) {
func (m *MockLogger) Debugf(arg0 adaptors.LogLevel, arg1 string, arg2 ...interface{}) {
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
@@ -199,7 +247,7 @@ func (mr *MockLoggerMockRecorder) Debugf(arg0, arg1 interface{}, arg2 ...interfa
}
// IsEnabled mocks base method
func (m *MockLogger) IsEnabled(arg0 interfaces.LogLevel) bool {
func (m *MockLogger) IsEnabled(arg0 adaptors.LogLevel) bool {
ret := m.ctrl.Call(m, "IsEnabled", arg0)
ret0, _ := ret[0].(bool)
return ret0
@@ -226,7 +274,7 @@ func (mr *MockLoggerMockRecorder) Printf(arg0 interface{}, arg1 ...interface{})
}
// SetLevel mocks base method
func (m *MockLogger) SetLevel(arg0 interfaces.LogLevel) {
func (m *MockLogger) SetLevel(arg0 adaptors.LogLevel) {
m.ctrl.Call(m, "SetLevel", arg0)
}

View File

@@ -1,73 +0,0 @@
package adaptors
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
func NewOIDC() adaptors.OIDC {
return &OIDC{}
}
type OIDC struct{}
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)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL())
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2cli.Config{
OAuth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID(),
ClientSecret: in.Config.ClientSecret(),
Scopes: append(in.Config.ExtraScopes(), oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
ShowLocalServerURL: cb.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID()})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (*OIDC) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*oidc.IDToken, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL())
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID()})
verifiedIDToken, err := verifier.Verify(ctx, in.Config.IDToken())
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return verifiedIDToken, nil
}

View File

@@ -1,4 +1,4 @@
package adaptors
package oidc
import (
"crypto/tls"
@@ -7,40 +7,30 @@ import (
"io/ioutil"
"net/http"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/infrastructure"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/oidc/logging"
"github.com/pkg/errors"
"go.uber.org/dig"
)
func NewHTTP(i HTTP) adaptors.HTTP {
return &i
}
type HTTP struct {
dig.In
Logger adaptors.Logger
}
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
func newHTTPClient(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*http.Client, error) {
pool := x509.NewCertPool()
if filename := config.OIDCConfig.IDPCertificateAuthority(); filename != "" {
h.Logger.Debugf(1, "Loading the certificate %s", filename)
err := appendCertificateFromFile(pool, filename)
if config.Config.IDPCertificateAuthority != "" {
logger.Debugf(1, "Loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority")
}
}
if data := config.OIDCConfig.IDPCertificateAuthorityData(); data != "" {
h.Logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, data)
if config.Config.IDPCertificateAuthorityData != "" {
logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority-data")
}
}
if config.CertificateAuthorityFilename != "" {
h.Logger.Debugf(1, "Loading the certificate %s", config.CertificateAuthorityFilename)
err := appendCertificateFromFile(pool, config.CertificateAuthorityFilename)
if config.CACertFilename != "" {
logger.Debugf(1, "Loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, errors.Wrapf(err, "could not load the certificate")
}
@@ -52,12 +42,12 @@ func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error)
}
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
return &http.Client{
Transport: &infrastructure.LoggingTransport{
Transport: &logging.Transport{
Base: &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
},
Logger: h.Logger,
Logger: logger,
},
}, nil
}

View File

@@ -1,10 +1,10 @@
package infrastructure
package logging
import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors"
)
const (
@@ -12,12 +12,12 @@ const (
logLevelDumpBody = 3
)
type LoggingTransport struct {
type Transport struct {
Base http.RoundTripper
Logger adaptors.Logger
}
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.IsDumpEnabled() {
return t.Base.RoundTrip(req)
}
@@ -41,10 +41,10 @@ func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
return resp, err
}
func (t *LoggingTransport) IsDumpEnabled() bool {
func (t *Transport) IsDumpEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpHeaders)
}
func (t *LoggingTransport) IsDumpBodyEnabled() bool {
func (t *Transport) IsDumpBodyEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpBody)
}

View File

@@ -1,4 +1,4 @@
package infrastructure
package logging
import (
"bufio"
@@ -8,7 +8,7 @@ import (
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
)
@@ -42,7 +42,7 @@ dummy`)), req)
}
defer resp.Body.Close()
transport := &LoggingTransport{
transport := &Transport{
Base: &mockTransport{resp: resp},
Logger: logger,
}
@@ -64,7 +64,7 @@ func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
Return(true)
transport := &LoggingTransport{
transport := &Transport{
Logger: logger,
}
if transport.IsDumpEnabled() != true {
@@ -81,7 +81,7 @@ func TestLoggingTransport_IsDumpBodyEnabled(t *testing.T) {
IsEnabled(adaptors.LogLevel(logLevelDumpBody)).
Return(true)
transport := &LoggingTransport{
transport := &Transport{
Logger: logger,
}
if transport.IsDumpBodyEnabled() != true {

118
adaptors/oidc/oidc.go Normal file
View File

@@ -0,0 +1,118 @@
package oidc
import (
"context"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
type Factory struct {
Logger adaptors.Logger
}
func (f *Factory) New(config adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
hc, err := newHTTPClient(config, f.Logger)
if err != nil {
return nil, errors.Wrapf(err, "could not create a HTTP client")
}
return &Client{hc}, nil
}
type Client struct {
hc *http.Client
}
func (c *Client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2cli.Config{
OAuth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID,
ClientSecret: in.Config.ClientSecret,
Scopes: append(in.Config.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
ShowLocalServerURL: in.ShowLocalServerURL.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (c *Client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
config := oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.Config.ClientID,
ClientSecret: in.Config.ClientSecret,
Scopes: append(in.Config.ExtraScopes, oidc.ScopeOpenID),
}
token, err := config.PasswordCredentialsToken(ctx, in.Username, in.Password)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (c *Client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*oidc.IDToken, error) {
if c.hc != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.hc)
}
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, in.Config.IDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return verifiedIDToken, nil
}

View File

@@ -1,39 +1,77 @@
// Package authserver provides an authentication server which supports
// Authorization Code Grant and Resource Owner Password Credentials Grant.
// This is only for testing.
//
package authserver
import (
"crypto/rsa"
"context"
"net"
"net/http"
"testing"
)
// Config represents server configuration.
type Config struct {
Addr string
Issuer string
Scope string
TLSServerCert string
TLSServerKey string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
type Shutdowner interface {
Shutdown(t *testing.T, ctx context.Context)
}
// Start starts a HTTP server.
func Start(t *testing.T, c Config) *http.Server {
type shutdowner struct {
l net.Listener
s *http.Server
}
func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
// s.Shutdown() closes the lister as well,
// so we do not need to call l.Close() explicitly
if err := s.s.Shutdown(ctx); err != nil {
t.Errorf("Could not shutdown the server: %s", err)
}
}
// Start starts an authentication server.
func Start(t *testing.T, h func(url string) http.Handler) Shutdowner {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
s := &http.Server{
Addr: c.Addr,
Handler: newHandler(t, c),
Handler: h(url),
}
go func() {
var err error
if c.TLSServerCert != "" && c.TLSServerKey != "" {
err = s.ListenAndServeTLS(c.TLSServerCert, c.TLSServerKey)
} else {
err = s.ListenAndServe()
}
err := s.Serve(l)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return s
return &shutdowner{l, s}
}
// Start starts an authentication server with TLS.
func StartTLS(t *testing.T, cert string, key string, h func(url string) http.Handler) Shutdowner {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
s := &http.Server{
Handler: h(url),
}
go func() {
err := s.ServeTLS(l, cert, key)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return &shutdowner{l, s}
}
func newLocalhostListener(t *testing.T) (net.Listener, string) {
t.Helper()
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Could not create a listener: %s", err)
}
addr := l.Addr().String()
_, port, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("Could not parse the address %s: %s", addr, err)
}
return l, port
}

View File

@@ -0,0 +1,107 @@
package authserver
import (
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"
"net/http"
"testing"
"github.com/pkg/errors"
)
// CodeConfig represents a config for Authorization Code Grant.
type CodeConfig struct {
Issuer string
Scope string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
Code string
}
type codeHandler struct {
t *testing.T
c CodeConfig
templates templates
values templateValues
}
func NewCodeHandler(t *testing.T, c CodeConfig) *codeHandler {
if c.Scope == "" {
c.Scope = "openid"
}
if c.Code == "" {
c.Code = "3d24a8bd-35e6-457d-999e-e04bb1dfcec7"
}
h := codeHandler{
t: t,
c: c,
templates: parseTemplates(t),
values: templateValues{
Issuer: c.Issuer,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
},
}
if c.IDTokenKeyPair != nil {
h.values.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.values.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *codeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("authserver/codeHandler: Error: %s", err)
w.WriteHeader(500)
}
}
func (h *codeHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("authserver/codeHandler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.discovery.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
if h.c.Scope != q.Get("scope") {
return errors.Errorf("scope wants %s but %s", h.c.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.c.Code)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
if err := r.ParseForm(); err != nil {
return errors.Wrapf(err, "could not parse the form")
}
grantType, code := r.Form.Get("grant_type"), r.Form.Get("code")
if grantType != "authorization_code" {
return errors.Errorf("grant_type wants authorization_code but %s", grantType)
}
if h.c.Code != code {
return errors.Errorf("code wants %s but %s", h.c.Code, code)
}
w.Header().Add("Content-Type", "application/json")
if err := h.templates.token.Execute(w, h.values); err != nil {
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.templates.jwks.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -1,108 +0,0 @@
package authserver
import (
"encoding/base64"
"fmt"
"math/big"
"net/http"
"testing"
"text/template"
"github.com/pkg/errors"
)
type handler struct {
t *testing.T
discovery *template.Template
token *template.Template
jwks *template.Template
authCode 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 {
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{
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"
}
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())
}
return &h
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
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 errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
if h.Scope != q.Get("scope") {
return errors.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.authCode)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
if err := r.ParseForm(); err != nil {
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 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 errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -0,0 +1,98 @@
package authserver
import (
"crypto/rsa"
"encoding/base64"
"math/big"
"net/http"
"testing"
"github.com/pkg/errors"
)
// PasswordConfig represents a config for Resource Owner Password Credentials Grant.
type PasswordConfig struct {
Issuer string
Scope string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
Username string
Password string
}
type passwordHandler struct {
t *testing.T
c PasswordConfig
templates templates
values templateValues
}
func NewPasswordHandler(t *testing.T, c PasswordConfig) *passwordHandler {
if c.Scope == "" {
c.Scope = "openid"
}
h := passwordHandler{
t: t,
c: c,
templates: parseTemplates(t),
values: templateValues{
Issuer: c.Issuer,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
},
}
if c.IDTokenKeyPair != nil {
h.values.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.values.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *passwordHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("authserver/passwordHandler: Error: %s", err)
w.WriteHeader(500)
}
}
func (h *passwordHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("authserver/passwordHandler: %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.templates.discovery.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// https://tools.ietf.org/html/rfc6749#section-4.3
if err := r.ParseForm(); err != nil {
return errors.Wrapf(err, "could not parse the form")
}
grantType, username, password := r.Form.Get("grant_type"), r.Form.Get("username"), r.Form.Get("password")
if grantType != "password" {
return errors.Errorf("grant_type wants password but %s", grantType)
}
if h.c.Username != username {
return errors.Errorf("username wants %s but %s", h.c.Username, username)
}
if h.c.Password != password {
return errors.Errorf("password wants %s but %s", h.c.Password, password)
}
w.Header().Add("Content-Type", "application/json")
if err := h.templates.token.Execute(w, h.values); err != nil {
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.templates.jwks.Execute(w, h.values); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -0,0 +1,35 @@
package authserver
import (
"testing"
"text/template"
)
func parseTemplates(t *testing.T) templates {
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)
}
return templates{
discovery: tpl.Lookup("oidc-discovery.json"),
token: tpl.Lookup("oidc-token.json"),
jwks: tpl.Lookup("oidc-jwks.json"),
}
}
type templates struct {
discovery *template.Template
token *template.Template
jwks *template.Template
}
type templateValues struct {
Issuer string
IDToken string
RefreshToken string
PrivateKey struct{ N, E string }
}

View File

@@ -10,7 +10,6 @@ import (
"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"
@@ -19,44 +18,73 @@ import (
)
// Run the integration tests.
// This assumes that port 800x and 900x are available.
//
// 1. Start the auth server at port 900x.
// 1. Start the auth server.
// 2. Run the Cmd.
// 3. Open a request for port 800x.
// 4. Wait for the Cmd.
// 5. Shutdown the auth server.
// 3. Open a request for the local server.
// 4. Verify the kuneconfig.
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("NoTLS", func(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9001")
serverConfig := authserver.Config{
Addr: "localhost:9001",
Issuer: "http://localhost:9001",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: codeConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8001", nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8001")
wg.Wait()
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var passwordConfig authserver.PasswordConfig
server := authserver.Start(t, func(url string) http.Handler {
passwordConfig = authserver.PasswordConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Username: "USER",
Password: "PASS",
}
return authserver.NewPasswordHandler(t, passwordConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: passwordConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, nil, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: passwordConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
@@ -66,31 +94,31 @@ func TestCmd_Run(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9002")
serverConfig := authserver.Config{
Addr: "localhost:9002",
Issuer: "http://localhost:9002",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: codeConfig.Issuer,
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8002", nil)
runCmd(t, ctx, "--skip-open-browser", "--listen-port", "8002")
wg.Wait()
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
@@ -100,30 +128,30 @@ func TestCmd_Run(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9003")
serverConfig := authserver.Config{
Addr: "localhost:9003",
Issuer: "http://localhost:9003",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: codeConfig.Issuer,
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8003", nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8003")
wg.Wait()
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
@@ -133,31 +161,29 @@ func TestCmd_Run(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9004")
serverConfig := authserver.Config{
Addr: "localhost:9004",
Issuer: "https://localhost:9004",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
TLSServerCert: keys.TLSServerCert,
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
var codeConfig authserver.CodeConfig
server := authserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: codeConfig.Issuer,
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8004", keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8004")
wg.Wait()
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
@@ -167,31 +193,29 @@ func TestCmd_Run(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9005")
serverConfig := authserver.Config{
Addr: "localhost:9005",
Issuer: "https://localhost:9005",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
TLSServerCert: keys.TLSServerCert,
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
var codeConfig authserver.CodeConfig
server := authserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDToken: newIDToken(t, url),
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: codeConfig.Issuer,
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
var wg sync.WaitGroup
startBrowserRequest(t, ctx, &wg, "http://localhost:8005", keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8005")
wg.Wait()
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
IDToken: codeConfig.IDToken,
RefreshToken: "REFRESH_TOKEN",
})
})
@@ -201,22 +225,24 @@ func TestCmd_Run(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
serverConfig := authserver.Config{
Addr: "localhost:9006",
Issuer: "http://localhost:9006",
IDTokenKeyPair: keys.JWSKeyPair,
}
server := authserver.Start(t, serverConfig)
defer shutdown(t, ctx, server)
var codeConfig authserver.CodeConfig
server := authserver.Start(t, func(url string) http.Handler {
codeConfig = authserver.CodeConfig{
Issuer: url,
IDTokenKeyPair: keys.JWSKeyPair,
}
return authserver.NewCodeHandler(t, codeConfig)
})
defer server.Shutdown(t, ctx)
idToken := newIDToken(t, serverConfig.Issuer)
idToken := newIDToken(t, codeConfig.Issuer)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: serverConfig.Issuer,
Issuer: codeConfig.Issuer,
IDToken: idToken,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
runCmd(t, ctx, nil, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
})
@@ -245,50 +271,61 @@ func newIDToken(t *testing.T, issuer string) string {
return s
}
func runCmd(t *testing.T, ctx context.Context, args ...string) {
func runCmd(t *testing.T, ctx context.Context, br *browserRequest, 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)
cmd := di.NewCmdWith(logger.New(t), br)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func startBrowserRequest(t *testing.T, ctx context.Context, wg *sync.WaitGroup, url string, tlsConfig *tls.Config) {
type browserRequest struct {
t *testing.T
urlCh chan<- string
wg *sync.WaitGroup
}
func (r *browserRequest) ShowLocalServerURL(url string) {
defer close(r.urlCh)
r.t.Logf("Open %s for authentication", url)
r.urlCh <- url
}
func (r *browserRequest) wait() {
r.wg.Wait()
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
t.Helper()
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
urlCh := make(chan string)
var wg sync.WaitGroup
go func() {
defer wg.Done()
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)
select {
case url := <-urlCh:
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
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)
}
case err := <-ctx.Done():
t.Errorf("context done while waiting for URL prompt: %s", err)
}
}()
wg.Add(1)
}
func shutdown(t *testing.T, ctx context.Context, s *http.Server) {
if err := s.Shutdown(ctx); err != nil {
t.Errorf("Could not shutdown the auth server: %s", err)
}
return &browserRequest{t, urlCh, &wg}
}
func setenv(t *testing.T, key, value string) {

View File

@@ -1,11 +1,11 @@
package logger
import (
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/logger"
)
func New(t testingLogger) *adaptors.Logger {
return adaptors.NewLoggerWith(&bridge{t})
func New(t testingLogger) *logger.Logger {
return logger.FromStdLogger(&bridge{t})
}
type testingLogger interface {

View File

@@ -1,42 +1,57 @@
//+build wireinject
// Package di provides dependency injection.
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
adaptorsInterfaces "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/cmd"
"github.com/int128/kubelogin/adaptors/env"
"github.com/int128/kubelogin/adaptors/kubeconfig"
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"go.uber.org/dig"
"github.com/int128/kubelogin/usecases/login"
)
var constructors = []interface{}{
usecases.NewLogin,
var usecasesSet = wire.NewSet(
login.Login{},
login.Exec{},
wire.Bind((*usecases.Login)(nil), (*login.Login)(nil)),
wire.Bind((*usecases.LoginAndExec)(nil), (*login.Exec)(nil)),
)
adaptors.NewCmd,
adaptors.NewKubeConfig,
adaptors.NewOIDC,
adaptors.NewHTTP,
}
var adaptorsSet = wire.NewSet(
cmd.Cmd{},
kubeconfig.Kubeconfig{},
oidc.Factory{},
env.Env{},
wire.Bind((*adaptors.Cmd)(nil), (*cmd.Cmd)(nil)),
wire.Bind((*adaptors.Kubeconfig)(nil), (*kubeconfig.Kubeconfig)(nil)),
wire.Bind((*adaptors.OIDC)(nil), (*oidc.Factory)(nil)),
wire.Bind((*adaptors.Env)(nil), (*env.Env)(nil)),
)
var extraConstructors = []interface{}{
adaptors.NewLogger,
}
var extraSet = wire.NewSet(
login.ShowLocalServerURL{},
wire.Bind((*usecases.LoginShowLocalServerURL)(nil), (*login.ShowLocalServerURL)(nil)),
logger.New,
)
// 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 append(constructors, extra...) {
if err := c.Provide(constructor); err != nil {
return errors.Wrapf(err, "could not provide the constructor")
}
}
if err := c.Invoke(f); err != nil {
return errors.Wrapf(err, "could not invoke")
}
func NewCmd() adaptors.Cmd {
wire.Build(
usecasesSet,
adaptorsSet,
extraSet,
)
return nil
}
func NewCmdWith(adaptors.Logger, usecases.LoginShowLocalServerURL) adaptors.Cmd {
wire.Build(
usecasesSet,
adaptorsSet,
)
return nil
}

View File

@@ -1,18 +0,0 @@
package di_test
import (
"testing"
adaptors "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
func TestInvoke(t *testing.T) {
if err := di.Invoke(func(cmd adaptors.Cmd) {
if cmd == nil {
t.Errorf("cmd wants non-nil but nil")
}
}); err != nil {
t.Fatalf("Invoke returned error: %+v", err)
}
}

88
di/wire_gen.go Normal file
View File

@@ -0,0 +1,88 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/cmd"
"github.com/int128/kubelogin/adaptors/env"
"github.com/int128/kubelogin/adaptors/kubeconfig"
"github.com/int128/kubelogin/adaptors/logger"
"github.com/int128/kubelogin/adaptors/oidc"
"github.com/int128/kubelogin/usecases"
"github.com/int128/kubelogin/usecases/login"
)
// Injectors from di.go:
func NewCmd() adaptors.Cmd {
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
adaptorsLogger := logger.New()
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
showLocalServerURL := &login.ShowLocalServerURL{
Logger: adaptorsLogger,
}
loginLogin := &login.Login{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
exec := &login.Exec{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
LoginAndExec: exec,
Logger: adaptorsLogger,
}
return cmdCmd
}
func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL) adaptors.Cmd {
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
factory := &oidc.Factory{
Logger: adaptorsLogger,
}
envEnv := &env.Env{}
loginLogin := &login.Login{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
exec := &login.Exec{
Kubeconfig: kubeconfigKubeconfig,
OIDC: factory,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
LoginAndExec: exec,
Logger: adaptorsLogger,
}
return cmdCmd
}
// di.go:
var usecasesSet = wire.NewSet(login.Login{}, login.Exec{}, wire.Bind((*usecases.Login)(nil), (*login.Login)(nil)), wire.Bind((*usecases.LoginAndExec)(nil), (*login.Exec)(nil)))
var adaptorsSet = wire.NewSet(cmd.Cmd{}, kubeconfig.Kubeconfig{}, oidc.Factory{}, env.Env{}, wire.Bind((*adaptors.Cmd)(nil), (*cmd.Cmd)(nil)), wire.Bind((*adaptors.Kubeconfig)(nil), (*kubeconfig.Kubeconfig)(nil)), wire.Bind((*adaptors.OIDC)(nil), (*oidc.Factory)(nil)), wire.Bind((*adaptors.Env)(nil), (*env.Env)(nil)))
var extraSet = wire.NewSet(login.ShowLocalServerURL{}, wire.Bind((*usecases.LoginShowLocalServerURL)(nil), (*login.ShowLocalServerURL)(nil)), logger.New)

View File

@@ -17,8 +17,6 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
### kops
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
@@ -53,7 +51,7 @@ You can create a custom role and assign it as well.
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
kubectl config set-credentials KUBECONTEXT \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
@@ -66,12 +64,9 @@ Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
```
Now your `~/.kube/config` should be like:

BIN
docs/keycloak-login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -67,7 +67,7 @@ You can create a custom role and assign it as well.
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
kubectl config set-credentials KUBECONTEXT \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
@@ -80,12 +80,9 @@ Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-16 22:03:13 +0900 JST
Updated ~/.kubeconfig
```
Now your `~/.kube/config` should be like:

6
go.mod
View File

@@ -2,11 +2,12 @@ module github.com/int128/kubelogin
require (
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-test/deep v1.0.1
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.3.1
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/google/wire v0.2.2
github.com/imdario/mergo v0.3.7 // indirect
github.com/int128/oauth2cli v1.4.0
github.com/json-iterator/go v1.1.6 // indirect
@@ -14,9 +15,10 @@ require (
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v0.0.4
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.3.0 // indirect
go.uber.org/dig v1.7.0
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect

52
go.sum
View File

@@ -1,31 +1,56 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/wire v0.2.2 h1:fSIRzE/K12IaNgV6X0173X/oLrTwHKRiMcFZhiDrN3s=
github.com/google/wire v0.2.2/go.mod h1:7FHVg6mFpFQrjeUZrm+BaD50N5jnDKm50uVPTpyYOmU=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/int128/oauth2cli v1.4.0 h1:Xt4uk2lIb9Mf9Xyd5o43Hf9iV5izb2jYK3zRX/cPgh0=
github.com/int128/oauth2cli v1.4.0/go.mod h1:81pWOyFVt1TRyZ7lZDtZuAGOE/S/jEpb1mpocRopI6U=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -34,31 +59,54 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.4 h1:S0tLZ3VOKl2Te0hpq8+ke0eSJPfCnNTPiDlsfwi1/NE=
github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
go.uber.org/dig v1.7.0/go.mod h1:z+dSd2TP9Usi48jL8M3v63iSBVkiwtVyMKxMZYYauPg=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 h1:HdqqaWmYAUI7/dmByKKEw+yxDksGSo+9GjkUc9Zp34E=
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

View File

@@ -1,56 +0,0 @@
package kubeconfig
import (
"strings"
)
// OIDCConfig represents config of an oidc auth-provider.
type OIDCConfig map[string]string
// IDPIssuerURL returns the idp-issuer-url.
func (c OIDCConfig) IDPIssuerURL() string {
return c["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c OIDCConfig) ClientID() string {
return c["client-id"]
}
// ClientSecret returns the client-secret.
func (c OIDCConfig) ClientSecret() string {
return c["client-secret"]
}
// IDPCertificateAuthority returns the idp-certificate-authority.
func (c OIDCConfig) IDPCertificateAuthority() string {
return c["idp-certificate-authority"]
}
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
func (c OIDCConfig) IDPCertificateAuthorityData() string {
return c["idp-certificate-authority-data"]
}
// ExtraScopes returns the extra-scopes.
func (c OIDCConfig) ExtraScopes() []string {
if c["extra-scopes"] == "" {
return []string{}
}
return strings.Split(c["extra-scopes"], ",")
}
// IDToken returns the id-token.
func (c OIDCConfig) IDToken() string {
return c["id-token"]
}
// SetIDToken replaces the id-token.
func (c OIDCConfig) SetIDToken(idToken string) {
c["id-token"] = idToken
}
// SetRefreshToken replaces the refresh-token.
func (c OIDCConfig) SetRefreshToken(refreshToken string) {
c["refresh-token"] = refreshToken
}

View File

@@ -1,66 +0,0 @@
// Package kubeconfig provides the models of kubeconfig file.
package kubeconfig
import (
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
type ContextName string
type UserName string
// Config represents a config.
type Config api.Config
// Context represents a context.
type Context api.Context
// User represents a user.
type User api.AuthInfo
// CurrentAuth represents the current authentication, that is,
// context, user and auth-provider.
type CurrentAuth struct {
ContextName ContextName // empty if UserName is given
Context *Context // nil if UserName is given
UserName UserName
User *User
OIDCConfig OIDCConfig
}
// FindCurrentAuth resolves the current context and user.
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func FindCurrentAuth(config *Config, contextName ContextName, userName UserName) (*CurrentAuth, error) {
var kubeContext *Context
if userName == "" {
if contextName == "" {
contextName = ContextName(config.CurrentContext)
}
contextNode := config.Contexts[string(contextName)]
if contextNode == nil {
return nil, errors.Errorf("context %s does not exist", contextName)
}
kubeContext = (*Context)(contextNode)
userName = UserName(kubeContext.AuthInfo)
}
userNode := config.AuthInfos[string(userName)]
if userNode == nil {
return nil, errors.Errorf("user %s does not exist", userName)
}
user := (*User)(userNode)
if user.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is missing")
}
if user.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider must be oidc but is %s", user.AuthProvider.Name)
}
return &CurrentAuth{
ContextName: contextName,
Context: kubeContext,
UserName: userName,
User: user,
OIDCConfig: user.AuthProvider.Config,
}, nil
}

View File

@@ -2,19 +2,13 @@ package main
import (
"context"
"log"
"os"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
var version = "HEAD"
func main() {
if err := di.Invoke(func(cmd adaptors.Cmd) {
os.Exit(cmd.Run(context.Background(), os.Args, version))
}); err != nil {
log.Fatalf("Error: %s", err)
}
os.Exit(di.NewCmd().Run(context.Background(), os.Args, version))
}

View File

@@ -0,0 +1,27 @@
package kubeconfig
// ContextName represents name of a context.
type ContextName string
// UserName represents name of a user.
type UserName string
// Auth represents the authentication provider,
// i.e. context, user and auth-provider in a kubeconfig.
type Auth struct {
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // Context name (optional)
OIDCConfig OIDCConfig
}
type OIDCConfig struct {
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id
ClientSecret string // client-secret
IDPCertificateAuthority string // (optional) idp-certificate-authority
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
ExtraScopes []string // (optional) extra-scopes
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
}

View File

@@ -1,23 +0,0 @@
package usecases
import (
"context"
"github.com/int128/kubelogin/kubeconfig"
)
//go:generate mockgen -package mock_usecases -destination ../mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases/interfaces Login
type Login interface {
Do(ctx context.Context, in LoginIn) error
}
type LoginIn struct {
KubeConfigFilename string // Default to the environment variable or global config as kubectl
KubeContextName kubeconfig.ContextName // Default to the current context but ignored if KubeUserName is set
KubeUserName kubeconfig.UserName // Default to the user of the context
CertificateAuthorityFilename string // Optional
SkipTLSVerify bool
SkipOpenBrowser bool
ListenPort []int
}

View File

@@ -1,123 +0,0 @@
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"
"github.com/pkg/errors"
"go.uber.org/dig"
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials CONTEXT_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
func NewLogin(i Login) usecases.Login {
return &i
}
type Login struct {
dig.In
KubeConfig adaptors.KubeConfig
HTTP adaptors.HTTP
OIDC adaptors.OIDC
Logger adaptors.Logger
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
mergedKubeConfig, err := u.KubeConfig.LoadByDefaultRules(in.KubeConfigFilename)
if err != nil {
return errors.Wrapf(err, "could not load the kubeconfig")
}
auth, err := kubeconfig.FindCurrentAuth(mergedKubeConfig, in.KubeContextName, in.KubeUserName)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage)
return errors.Wrapf(err, "could not find the current authentication provider")
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
destinationKubeConfigFilename := auth.User.LocationOfOrigin
if destinationKubeConfigFilename == "" {
return errors.Errorf("could not determine the kubeconfig to write")
}
u.Logger.Debugf(1, "A token will be written to %s", destinationKubeConfigFilename)
hc, err := u.HTTP.NewClient(adaptors.HTTPClientConfig{
OIDCConfig: auth.OIDCConfig,
CertificateAuthorityFilename: in.CertificateAuthorityFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not set up a HTTP client")
}
if auth.OIDCConfig.IDToken() != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := u.OIDC.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig, Client: hc})
if err == nil {
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
u.dumpIDToken(token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
out, err := u.OIDC.Authenticate(ctx,
adaptors.OIDCAuthenticateIn{
Config: auth.OIDCConfig,
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 a token from the OIDC provider")
}
u.Logger.Printf("You got a valid token until %s", out.VerifiedIDToken.Expiry)
u.dumpIDToken(out.VerifiedIDToken)
if err := u.writeToken(destinationKubeConfigFilename, auth.UserName, out); err != nil {
return errors.Wrapf(err, "could not write the token to the 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) writeToken(filename string, userName kubeconfig.UserName, out *adaptors.OIDCAuthenticateOut) error {
config, err := u.KubeConfig.LoadFromFile(filename)
if err != nil {
return errors.Wrapf(err, "could not load %s", filename)
}
auth, err := kubeconfig.FindCurrentAuth(config, "", userName)
if err != nil {
return errors.Wrapf(err, "could not find the user %s in %s", userName, filename)
}
auth.OIDCConfig.SetIDToken(out.IDToken)
auth.OIDCConfig.SetRefreshToken(out.RefreshToken)
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", filename)
if err := u.KubeConfig.WriteToFile(config, filename); err != nil {
return errors.Wrapf(err, "could not update %s", filename)
}
u.Logger.Printf("Updated %s", filename)
return nil
}

104
usecases/login/exec.go Normal file
View File

@@ -0,0 +1,104 @@
package login
import (
"context"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
// Exec provide the use case of wrapping the kubectl command.
// If the current auth provider is not oidc, just run kubectl.
// If the kubeconfig has a valid token, just run kubectl.
// Otherwise, update the kubeconfig and run kubectl.
type Exec struct {
Kubeconfig adaptors.Kubeconfig
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Exec) Do(ctx context.Context, in usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
if err := u.doInternal(ctx, in.LoginIn); err != nil {
return nil, errors.WithStack(err)
}
exitCode, err := u.Env.Exec(ctx, in.Executable, in.Args)
if err != nil {
return nil, errors.Wrapf(err, "could not execute kubectl")
}
return &usecases.LoginAndExecOut{ExitCode: exitCode}, nil
}
func (u *Exec) doInternal(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
auth, err := u.Kubeconfig.GetCurrentAuth(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Debugf(1, "The current authentication provider is not oidc: %s", err)
return nil
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
u.Logger.Debugf(1, "A token will be written to %s", auth.LocationOfOrigin)
client, err := u.OIDC.New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not create an OIDC client")
}
if auth.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := client.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig})
if err == nil {
u.Logger.Debugf(1, "You already have a valid token until %s", token.Expiry)
dumpIDToken(u.Logger, token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
var tokenSet *adaptors.OIDCAuthenticateOut
if in.Username != "" {
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return errors.Wrapf(err, "could not read a password")
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: in.Username,
Password: in.Password,
})
if err != nil {
return errors.Wrapf(err, "error while the resource owner password credentials grant flow")
}
tokenSet = out
} else {
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return errors.Wrapf(err, "error while the authorization code grant flow")
}
tokenSet = out
}
u.Logger.Printf("You got a valid token until %s", tokenSet.VerifiedIDToken.Expiry)
dumpIDToken(u.Logger, tokenSet.VerifiedIDToken)
auth.OIDCConfig.IDToken = tokenSet.IDToken
auth.OIDCConfig.RefreshToken = tokenSet.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", auth.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuth(auth); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
}
return nil
}

View File

@@ -0,0 +1,98 @@
package login
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
func TestExec_Do(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: []int{10000},
}), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
u := Exec{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: []int{10000},
},
Executable: "kubectl",
Args: []string{"foo", "bar"},
})
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
if out.ExitCode != 0 {
t.Errorf("ExitCode wants 0 but %d", out.ExitCode)
}
})
t.Run("NoOIDCConfig", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, errors.New("no oidc config"))
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().
Exec(ctx, "kubectl", []string{"foo", "bar"}).
Return(0, nil)
u := Exec{
Kubeconfig: mockKubeconfig,
OIDC: mock_adaptors.NewMockOIDC(ctrl),
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
out, err := u.Do(ctx, usecases.LoginAndExecIn{
LoginIn: usecases.LoginIn{
ListenPort: []int{10000},
},
Executable: "kubectl",
Args: []string{"foo", "bar"},
})
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
if out.ExitCode != 0 {
t.Errorf("ExitCode wants 0 but %d", out.ExitCode)
}
})
}

122
usecases/login/login.go Normal file
View File

@@ -0,0 +1,122 @@
package login
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials CONTEXT_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
const passwordPrompt = "Password: "
// Login provides the use case of login to the provider.
// If the current auth provider is not oidc, show the error.
// If the kubeconfig has a valid token, do nothing.
// Otherwise, update the kubeconfig.
type Login struct {
Kubeconfig adaptors.Kubeconfig
OIDC adaptors.OIDC
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
auth, err := u.Kubeconfig.GetCurrentAuth(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
u.Logger.Printf(oidcConfigErrorMessage)
return errors.Wrapf(err, "could not find the current authentication provider")
}
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
u.Logger.Debugf(1, "A token will be written to %s", auth.LocationOfOrigin)
client, err := u.OIDC.New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return errors.Wrapf(err, "could not create an OIDC client")
}
if auth.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
token, err := client.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig})
if err == nil {
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
dumpIDToken(u.Logger, token)
return nil
}
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
}
var tokenSet *adaptors.OIDCAuthenticateOut
if in.Username != "" {
if in.Password == "" {
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return errors.Wrapf(err, "could not read a password")
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: in.Username,
Password: in.Password,
})
if err != nil {
return errors.Wrapf(err, "error while the resource owner password credentials grant flow")
}
tokenSet = out
} else {
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return errors.Wrapf(err, "error while the authorization code grant flow")
}
tokenSet = out
}
u.Logger.Printf("You got a valid token until %s", tokenSet.VerifiedIDToken.Expiry)
dumpIDToken(u.Logger, tokenSet.VerifiedIDToken)
auth.OIDCConfig.IDToken = tokenSet.IDToken
auth.OIDCConfig.RefreshToken = tokenSet.RefreshToken
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", auth.LocationOfOrigin)
if err := u.Kubeconfig.UpdateAuth(auth); err != nil {
return errors.Wrapf(err, "could not write the token to the kubeconfig")
}
return nil
}
func dumpIDToken(logger adaptors.Logger, token *oidc.IDToken) {
var claims map[string]interface{}
if err := token.Claims(&claims); err != nil {
logger.Debugf(1, "Error while inspection of the ID token: %s", err)
}
for k, v := range claims {
logger.Debugf(1, "The ID token has the claim: %s=%v", k, v)
}
}
// ShowLocalServerURL just shows the URL of local server to console.
type ShowLocalServerURL struct {
Logger adaptors.Logger
}
func (s *ShowLocalServerURL) ShowLocalServerURL(url string) {
s.Logger.Printf("Open %s for authentication", url)
}

View File

@@ -0,0 +1,264 @@
package login
import (
"context"
"testing"
"time"
"github.com/coreos/go-oidc"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/models/kubeconfig"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
)
func newMockCodeOIDC(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) *mock_adaptors.MockOIDCClient {
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, in).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDCClient
}
func newMockPasswordOIDC(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) *mock_adaptors.MockOIDCClient {
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, in).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDCClient
}
func newAuth(idToken, refreshToken string) *kubeconfig.Auth {
return &kubeconfig.Auth{
LocationOfOrigin: "theLocation",
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: idToken,
RefreshToken: refreshToken,
},
}
}
func TestLogin_Do(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{
Config: auth.OIDCConfig,
LocalServerPort: []int{10000},
}), nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{
Config: auth.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(newMockPasswordOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: "USER",
Password: "PASS",
}), nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("AskPassword", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(newMockPasswordOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByPasswordIn{
Config: auth.OIDCConfig,
Username: "USER",
Password: "PASS",
}), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Env: mockEnv,
}
if err := u.Do(ctx, usecases.LoginIn{
Username: "USER",
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("AskPasswordError", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("", "")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mock_adaptors.NewMockOIDCClient(ctrl), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", errors.New("error"))
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
Env: mockEnv,
}
if err := u.Do(ctx, usecases.LoginIn{
Username: "USER",
}); err == nil {
t.Errorf("err wants an error but nil")
}
})
t.Run("KubeconfigHasValidToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("VALID_ID_TOKEN", "N/A")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig}).
Return(&oidc.IDToken{Expiry: time.Now()}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mockOIDCClient, nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeconfigHasExpiredToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
auth := newAuth("EXPIRED_ID_TOKEN", "EXPIRED_REFRESH_TOKEN")
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuth("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(auth, nil)
mockKubeconfig.EXPECT().
UpdateAuth(newAuth("YOUR_ID_TOKEN", "YOUR_REFRESH_TOKEN"))
mockOIDCClient := newMockCodeOIDC(ctrl, ctx, adaptors.OIDCAuthenticateByCodeIn{Config: auth.OIDCConfig})
mockOIDCClient.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig}).
Return(nil, errors.New("token expired"))
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(adaptors.OIDCClientConfig{Config: auth.OIDCConfig}).
Return(mockOIDCClient, nil)
u := Login{
Kubeconfig: mockKubeconfig,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
}

View File

@@ -1,586 +0,0 @@
package usecases
import (
"context"
"net/http"
"testing"
"github.com/coreos/go-oidc"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/kubeconfig"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
type loginTestFixture struct {
googleOIDCConfig kubeconfig.OIDCConfig
googleOIDCConfigWithToken kubeconfig.OIDCConfig
googleKubeConfig *kubeconfig.Config
googleKubeConfigWithToken *kubeconfig.Config
keycloakOIDCConfig kubeconfig.OIDCConfig
keycloakOIDCConfigWithToken kubeconfig.OIDCConfig
keycloakKubeConfig *kubeconfig.Config
keycloakKubeConfigWithToken *kubeconfig.Config
mergedKubeConfig *kubeconfig.Config
}
func newLoginTestFixture() loginTestFixture {
var f loginTestFixture
f.googleOIDCConfig = kubeconfig.OIDCConfig{
"client-id": "GOOGLE_CLIENT_ID",
"client-secret": "GOOGLE_CLIENT_SECRET",
"idp-issuer-url": "https://accounts.google.com",
}
f.googleKubeConfig = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"googleContext": {
LocationOfOrigin: "/path/to/google",
AuthInfo: "google",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
LocationOfOrigin: "/path/to/google",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.googleOIDCConfig,
},
},
},
}
f.googleOIDCConfigWithToken = kubeconfig.OIDCConfig{
"client-id": "GOOGLE_CLIENT_ID",
"client-secret": "GOOGLE_CLIENT_SECRET",
"idp-issuer-url": "https://accounts.google.com",
"id-token": "YOUR_ID_TOKEN",
"refresh-token": "YOUR_REFRESH_TOKEN",
}
f.googleKubeConfigWithToken = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"googleContext": {
LocationOfOrigin: "/path/to/google",
AuthInfo: "google",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
LocationOfOrigin: "/path/to/google",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.googleOIDCConfigWithToken,
},
},
},
}
f.keycloakOIDCConfig = kubeconfig.OIDCConfig{
"client-id": "KEYCLOAK_CLIENT_ID",
"client-secret": "KEYCLOAK_CLIENT_SECRET",
"idp-issuer-url": "https://keycloak.example.com",
}
f.keycloakKubeConfig = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"keycloakContext": {
LocationOfOrigin: "/path/to/keycloak",
AuthInfo: "keycloak",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"keycloak": {
LocationOfOrigin: "/path/to/keycloak",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.keycloakOIDCConfig,
},
},
},
}
f.keycloakOIDCConfigWithToken = kubeconfig.OIDCConfig{
"client-id": "KEYCLOAK_CLIENT_ID",
"client-secret": "KEYCLOAK_CLIENT_SECRET",
"idp-issuer-url": "https://keycloak.example.com",
"id-token": "YOUR_ID_TOKEN",
"refresh-token": "YOUR_REFRESH_TOKEN",
}
f.keycloakKubeConfigWithToken = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"keycloakContext": {
LocationOfOrigin: "/path/to/keycloak",
AuthInfo: "keycloak",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"keycloak": {
LocationOfOrigin: "/path/to/keycloak",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.keycloakOIDCConfigWithToken,
},
},
},
}
f.mergedKubeConfig = &kubeconfig.Config{
APIVersion: "v1",
CurrentContext: "googleContext",
Contexts: map[string]*api.Context{
"googleContext": {
LocationOfOrigin: "/path/to/google",
AuthInfo: "google",
Cluster: "example.k8s.local",
},
"keycloakContext": {
LocationOfOrigin: "/path/to/keycloak",
AuthInfo: "keycloak",
Cluster: "example.k8s.local",
},
},
AuthInfos: map[string]*api.AuthInfo{
"google": {
LocationOfOrigin: "/path/to/google",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.googleOIDCConfig,
},
},
"keycloak": {
LocationOfOrigin: "/path/to/keycloak",
AuthProvider: &api.AuthProviderConfig{
Name: "oidc",
Config: f.keycloakOIDCConfig,
},
},
},
}
return f
}
func TestLogin_Do(t *testing.T) {
httpClient := &http.Client{}
newMockOIDC := func(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateIn) *mock_adaptors.MockOIDC {
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Authenticate(ctx, in, gomock.Any()).
Do(func(_ context.Context, _ adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) {
cb.ShowLocalServerURL("http://localhost:10000")
}).
Return(&adaptors.OIDCAuthenticateOut{
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
}, nil)
return mockOIDC
}
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfigFilename", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("/path/to/kubeconfig").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeConfigFilename: "/path/to/kubeconfig",
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeContextName", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.keycloakOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/keycloak").
Return(f.keycloakKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.keycloakKubeConfigWithToken, "/path/to/keycloak")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.keycloakOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeContextName: "keycloakContext",
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeUserName", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.keycloakOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/keycloak").
Return(f.keycloakKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.keycloakKubeConfigWithToken, "/path/to/keycloak")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.keycloakOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
KubeUserName: "keycloak",
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
SkipTLSVerify: true,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
SkipTLSVerify: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("SkipOpenBrowser", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
SkipOpenBrowser: true,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
SkipOpenBrowser: true,
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/ValidToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
f.googleOIDCConfig.SetIDToken("VALID_TOKEN")
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{
Config: f.googleOIDCConfig,
Client: httpClient,
}).
Return(&oidc.IDToken{}, nil)
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("KubeConfig/InvalidToken", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
f.googleOIDCConfig.SetIDToken("EXPIRED_TOKEN")
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
mockOIDC := newMockOIDC(ctrl, ctx, adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
})
mockOIDC.EXPECT().
Verify(ctx, adaptors.OIDCVerifyIn{
Config: f.googleOIDCConfig,
Client: httpClient,
}).
Return(nil, errors.New("token is expired"))
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
t.Run("Certificates", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
f := newLoginTestFixture()
f.googleOIDCConfig["idp-certificate-authority"] = "/path/to/cert2"
f.googleOIDCConfig["idp-certificate-authority-data"] = "base64encoded"
f.googleOIDCConfigWithToken["idp-certificate-authority"] = "/path/to/cert2"
f.googleOIDCConfigWithToken["idp-certificate-authority-data"] = "base64encoded"
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
mockHTTP.EXPECT().
NewClient(adaptors.HTTPClientConfig{
OIDCConfig: f.googleOIDCConfig,
CertificateAuthorityFilename: "/path/to/cert1",
}).
Return(httpClient, nil)
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
mockKubeConfig.EXPECT().
LoadByDefaultRules("").
Return(f.mergedKubeConfig, nil)
mockKubeConfig.EXPECT().
LoadFromFile("/path/to/google").
Return(f.googleKubeConfig, nil)
mockKubeConfig.EXPECT().
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
oidcIn := adaptors.OIDCAuthenticateIn{
Config: f.googleOIDCConfig,
LocalServerPort: []int{10000},
Client: httpClient,
}
u := Login{
KubeConfig: mockKubeConfig,
HTTP: mockHTTP,
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
if err := u.Do(ctx, usecases.LoginIn{
ListenPort: []int{10000},
CertificateAuthorityFilename: "/path/to/cert1",
}); err != nil {
t.Errorf("Do returned error: %+v", err)
}
})
}

View File

@@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/usecases/interfaces (interfaces: Login)
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
@@ -7,7 +7,7 @@ package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/usecases/interfaces"
usecases "github.com/int128/kubelogin/usecases"
reflect "reflect"
)
@@ -35,7 +35,7 @@ func (m *MockLogin) EXPECT() *MockLoginMockRecorder {
}
// Do mocks base method
func (m *MockLogin) Do(arg0 context.Context, arg1 interfaces.LoginIn) error {
func (m *MockLogin) Do(arg0 context.Context, arg1 usecases.LoginIn) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
@@ -45,3 +45,39 @@ func (m *MockLogin) Do(arg0 context.Context, arg1 interfaces.LoginIn) error {
func (mr *MockLoginMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLogin)(nil).Do), arg0, arg1)
}
// MockLoginAndExec is a mock of LoginAndExec interface
type MockLoginAndExec struct {
ctrl *gomock.Controller
recorder *MockLoginAndExecMockRecorder
}
// MockLoginAndExecMockRecorder is the mock recorder for MockLoginAndExec
type MockLoginAndExecMockRecorder struct {
mock *MockLoginAndExec
}
// NewMockLoginAndExec creates a new mock instance
func NewMockLoginAndExec(ctrl *gomock.Controller) *MockLoginAndExec {
mock := &MockLoginAndExec{ctrl: ctrl}
mock.recorder = &MockLoginAndExecMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLoginAndExec) EXPECT() *MockLoginAndExecMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockLoginAndExec) Do(arg0 context.Context, arg1 usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*usecases.LoginAndExecOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockLoginAndExecMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLoginAndExec)(nil).Do), arg0, arg1)
}

45
usecases/usecases.go Normal file
View File

@@ -0,0 +1,45 @@
package usecases
import (
"context"
"github.com/int128/kubelogin/models/kubeconfig"
)
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec
type Login interface {
Do(ctx context.Context, in LoginIn) error
}
type LoginIn struct {
KubeconfigFilename string // Default to the environment variable or global config as kubectl
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
KubeconfigUser kubeconfig.UserName // Default to the user of the context
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
}
// LoginShowLocalServerURL provides an interface to notify the URL of local server.
// It is needed for the end-to-end tests.
type LoginShowLocalServerURL interface {
ShowLocalServerURL(url string)
}
type LoginAndExec interface {
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
}
type LoginAndExecIn struct {
LoginIn LoginIn
Executable string
Args []string
}
type LoginAndExecOut struct {
ExitCode int
}