mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-28 16:00:19 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97b0bdd53c | ||
|
|
ebe5feaed2 | ||
|
|
4427bc7985 | ||
|
|
8643d695fe | ||
|
|
f0cff5a54b | ||
|
|
8237928af3 | ||
|
|
01b270755b | ||
|
|
e97b4de40b | ||
|
|
5063550468 | ||
|
|
d5989ca256 | ||
|
|
c508a1b717 | ||
|
|
e133ea8541 | ||
|
|
3f2e84a1ea | ||
|
|
7011f03094 | ||
|
|
6aef98cef7 | ||
|
|
93bb1d39b9 | ||
|
|
c8116e2eae | ||
|
|
f2de8dd987 | ||
|
|
915fb35bc8 | ||
|
|
51ccd70af3 | ||
|
|
c6df597fb0 | ||
|
|
ee78f6f735 | ||
|
|
6e484a2b89 | ||
|
|
8050db7e05 | ||
|
|
cd54ca0df0 | ||
|
|
45f83b0b0e | ||
|
|
51b7ca1600 | ||
|
|
83f85a9b53 | ||
|
|
d82c8a2dd1 | ||
|
|
072bee6992 | ||
|
|
5c07850a68 | ||
|
|
5c8c80f055 | ||
|
|
bc7bfabfb2 | ||
|
|
73112546de |
@@ -11,15 +11,17 @@ jobs:
|
||||
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
|
||||
chmod +x ~/bin/kubectl
|
||||
- run: |
|
||||
go get -v \
|
||||
golang.org/x/lint/golint \
|
||||
github.com/int128/goxzst \
|
||||
github.com/tcnksm/ghr \
|
||||
github.com/int128/ghcp
|
||||
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.3.0/ghcp_linux_amd64
|
||||
chmod +x ~/bin/ghcp
|
||||
- run: |
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.16.0
|
||||
- run: go get github.com/int128/goxzst
|
||||
- run: go get github.com/tcnksm/ghr
|
||||
- checkout
|
||||
# workaround for https://github.com/golang/go/issues/27925
|
||||
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
|
||||
- run: make check
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: make run
|
||||
- run: |
|
||||
if [ "$CIRCLE_TAG" ]; then
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
/.idea
|
||||
|
||||
/.kubeconfig*
|
||||
|
||||
/dist
|
||||
/coverage.out
|
||||
|
||||
/kubelogin
|
||||
/kubectl-oidc_login
|
||||
/.kubeconfig
|
||||
|
||||
5
Makefile
5
Makefile
@@ -8,10 +8,9 @@ LDFLAGS := -X main.version=$(CIRCLE_TAG)
|
||||
all: $(TARGET)
|
||||
|
||||
check:
|
||||
golint
|
||||
go vet
|
||||
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)"
|
||||
|
||||
225
README.md
225
README.md
@@ -1,18 +1,11 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)
|
||||
# kubelogin [](https://circleci.com/gh/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).
|
||||
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
|
||||
@@ -24,29 +17,17 @@ brew install kubelogin
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.1/kubelogin_linux_amd64.zip
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.11.0/kubelogin_linux_amd64.zip
|
||||
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
|
||||
Using current-context: hello.k8s.local
|
||||
Open http://localhost:8000 for authorization
|
||||
Got a token for subject 0123456789 (valid until 2019-04-12 11:00:49 +0900 JST)
|
||||
Updated ~/.kube/config
|
||||
```
|
||||
|
||||
or run as a kubectl plugin:
|
||||
|
||||
```
|
||||
% 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:
|
||||
|
||||
@@ -55,27 +36,125 @@ For more, see the following documents:
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
This supports the following options.
|
||||
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 [OPTIONS]
|
||||
Usage:
|
||||
kubelogin [flags]
|
||||
kubelogin [command]
|
||||
|
||||
Application Options:
|
||||
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
|
||||
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
|
||||
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
|
||||
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
|
||||
-v, --v= If set to 1 or greater, show debug log (default: 0)
|
||||
Examples:
|
||||
# Login to the provider using authorization code grant.
|
||||
kubelogin
|
||||
|
||||
Help Options:
|
||||
-h, --help Show this help message
|
||||
# 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
|
||||
--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
|
||||
```
|
||||
|
||||
This also supports the following keys of `auth-provider` in kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
|
||||
It supports the following keys of `auth-provider` in a kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
@@ -89,13 +168,67 @@ Key | Direction | Value
|
||||
`refresh-token` | Write | Refresh token got from the provider.
|
||||
|
||||
|
||||
### Kubeconfig path
|
||||
### Kubeconfig
|
||||
|
||||
You can set the environment variable `KUBECONFIG` to point the config file.
|
||||
Default to `~/.kube/config`.
|
||||
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
|
||||
It defaults to `~/.kube/config`.
|
||||
|
||||
```sh
|
||||
export KUBECONFIG="$PWD/.kubeconfig"
|
||||
# by the option
|
||||
kubelogin --kubeconfig /path/to/kubeconfig
|
||||
|
||||
# by the environment variable
|
||||
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:
|
||||
```
|
||||
|
||||
|
||||
@@ -117,15 +250,13 @@ sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
|
||||
|
||||
### CA Certificates
|
||||
|
||||
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by `idp-certificate-authority` and `idp-certificate-authority-data` in the kubeconfig.
|
||||
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by kubeconfig or option.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak \
|
||||
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
|
||||
```
|
||||
|
||||
If kubelogin could not parse the certificate, it shows a warning and skips it.
|
||||
|
||||
|
||||
### HTTP Proxy
|
||||
|
||||
|
||||
79
adaptors/adaptors.go
Normal file
79
adaptors/adaptors.go
Normal 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
|
||||
@@ -1,75 +0,0 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
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 {
|
||||
var o cmdOptions
|
||||
parser := flags.NewParser(&o, flags.HelpFlag)
|
||||
parser.LongDescription = fmt.Sprintf(`Version %s
|
||||
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
|
||||
version)
|
||||
args, err := parser.ParseArgs(args[1:])
|
||||
if err != nil {
|
||||
cmd.Logger.Printf("Error: %s", err)
|
||||
return 1
|
||||
}
|
||||
if len(args) > 0 {
|
||||
cmd.Logger.Printf("Error: too many arguments")
|
||||
return 1
|
||||
}
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
kubeConfig, err := o.ExpandKubeConfig()
|
||||
if err != nil {
|
||||
cmd.Logger.Printf("Error: invalid option: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
in := usecases.LoginIn{
|
||||
KubeConfig: kubeConfig,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
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 `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
|
||||
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
|
||||
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
|
||||
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
|
||||
Verbose int `long:"v" short:"v" default:"0" description:"If set to 1 or greater, show debug log"`
|
||||
}
|
||||
|
||||
// ExpandKubeConfig returns an expanded KubeConfig path.
|
||||
func (c *cmdOptions) ExpandKubeConfig() (string, error) {
|
||||
d, err := homedir.Expand(c.KubeConfig)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
168
adaptors/cmd/cmd.go
Normal file
168
adaptors/cmd/cmd.go
Normal 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
268
adaptors/cmd/cmd_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,98 +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"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
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{
|
||||
KubeConfig: expand(t, "~/.kube/config"),
|
||||
ListenPort: 8000,
|
||||
})
|
||||
|
||||
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{
|
||||
KubeConfig: expand(t, "~/.kube/config"),
|
||||
ListenPort: 10080,
|
||||
SkipTLSVerify: true,
|
||||
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,
|
||||
"--listen-port", "10080",
|
||||
"--insecure-skip-tls-verify",
|
||||
"--skip-open-browser",
|
||||
"-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 expand(t *testing.T, path string) string {
|
||||
d, err := homedir.Expand(path)
|
||||
if err != nil {
|
||||
t.Fatalf("could not expand: %s", err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
44
adaptors/env/env.go
vendored
Normal file
44
adaptors/env/env.go
vendored
Normal 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
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/infrastructure"
|
||||
"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 (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
|
||||
return &httpClientConfig{
|
||||
certPool: x509.NewCertPool(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
|
||||
return &http.Client{
|
||||
Transport: &infrastructure.LoggingTransport{
|
||||
Base: &http.Transport{
|
||||
TLSClientConfig: config.TLSConfig(),
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Logger: h.Logger,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type httpClientConfig struct {
|
||||
certPool *x509.CertPool
|
||||
skipTLSVerify bool
|
||||
}
|
||||
|
||||
func (c *httpClientConfig) AddCertificateFromFile(filename string) error {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read %s", filename)
|
||||
}
|
||||
if c.certPool.AppendCertsFromPEM(b) != true {
|
||||
return errors.Errorf("could not append certificate from %s", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *httpClientConfig) AddEncodedCertificate(base64String string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not decode base64")
|
||||
}
|
||||
if c.certPool.AppendCertsFromPEM(b) != true {
|
||||
return errors.Errorf("could not append certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *httpClientConfig) TLSConfig() *tls.Config {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: c.skipTLSVerify,
|
||||
}
|
||||
if len(c.certPool.Subjects()) > 0 {
|
||||
tlsConfig.RootCAs = c.certPool
|
||||
}
|
||||
return tlsConfig
|
||||
}
|
||||
|
||||
func (c *httpClientConfig) SetSkipTLSVerify(b bool) {
|
||||
c.skipTLSVerify = b
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger
|
||||
|
||||
type Cmd interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
}
|
||||
|
||||
type KubeConfig interface {
|
||||
LoadFromFile(filename string) (*api.Config, error)
|
||||
WriteToFile(config *api.Config, filename string) error
|
||||
}
|
||||
|
||||
type HTTP interface {
|
||||
NewClientConfig() HTTPClientConfig
|
||||
NewClient(config HTTPClientConfig) (*http.Client, error)
|
||||
}
|
||||
|
||||
type HTTPClientConfig interface {
|
||||
AddCertificateFromFile(filename string) error
|
||||
AddEncodedCertificate(base64String string) error
|
||||
SetSkipTLSVerify(b bool)
|
||||
|
||||
TLSConfig() *tls.Config
|
||||
}
|
||||
|
||||
type OIDC interface {
|
||||
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
|
||||
VerifyIDToken(ctx context.Context, in OIDCVerifyTokenIn) (*oidc.IDToken, error)
|
||||
}
|
||||
|
||||
type OIDCAuthenticateIn struct {
|
||||
Issuer string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // Additional scopes
|
||||
Client *http.Client // HTTP client for oidc and oauth2
|
||||
LocalServerPort int // HTTP server port
|
||||
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 OIDCVerifyTokenIn struct {
|
||||
IDToken string
|
||||
Issuer string
|
||||
ClientID string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Debugf(level LogLevel, format string, v ...interface{})
|
||||
SetLevel(level LogLevel)
|
||||
IsEnabled(level LogLevel) bool
|
||||
}
|
||||
|
||||
// LogLevel represents a log level for debug.
|
||||
//
|
||||
// 0 = None
|
||||
// 1 = Including in/out
|
||||
// 2 = Including transport headers
|
||||
// 3 = Including transport body
|
||||
//
|
||||
type LogLevel int
|
||||
@@ -1,30 +0,0 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"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{}
|
||||
|
||||
func (*KubeConfig) LoadFromFile(filename string) (*api.Config, error) {
|
||||
config, err := clientcmd.LoadFromFile(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read the kubeconfig from %s", filename)
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (*KubeConfig) WriteToFile(config *api.Config, filename string) error {
|
||||
err := clientcmd.WriteToFile(*config, filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not write the kubeconfig to %s", filename)
|
||||
}
|
||||
return err
|
||||
}
|
||||
3
adaptors/kubeconfig/kubeconfig.go
Normal file
3
adaptors/kubeconfig/kubeconfig.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package kubeconfig
|
||||
|
||||
type Kubeconfig struct{}
|
||||
85
adaptors/kubeconfig/load.go
Normal file
85
adaptors/kubeconfig/load.go
Normal 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"],
|
||||
}
|
||||
}
|
||||
225
adaptors/kubeconfig/load_test.go
Normal file
225
adaptors/kubeconfig/load_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
17
adaptors/kubeconfig/testdata/kubeconfig.google.yaml
vendored
Normal file
17
adaptors/kubeconfig/testdata/kubeconfig.google.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
clusters: []
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: google
|
||||
name: google@hello.k8s.local
|
||||
current-context: google@hello.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: google
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: CLIENT_ID.apps.googleusercontent.com
|
||||
name: oidc
|
||||
16
adaptors/kubeconfig/testdata/kubeconfig.keycloak.yaml
vendored
Normal file
16
adaptors/kubeconfig/testdata/kubeconfig.keycloak.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: keycloak
|
||||
name: keycloak@hello.k8s.local
|
||||
current-context: keycloak@hello.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: kubernetes
|
||||
name: oidc
|
||||
51
adaptors/kubeconfig/write.go
Normal file
51
adaptors/kubeconfig/write.go
Normal 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
|
||||
}
|
||||
138
adaptors/kubeconfig/write_test.go
Normal file
138
adaptors/kubeconfig/write_test.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,183 +1,64 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,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
|
||||
|
||||
import (
|
||||
context "context"
|
||||
tls "crypto/tls"
|
||||
go_oidc "github.com/coreos/go-oidc"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
|
||||
api "k8s.io/client-go/tools/clientcmd/api"
|
||||
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
|
||||
}
|
||||
|
||||
// LoadFromFile mocks base method
|
||||
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*api.Config, error) {
|
||||
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
|
||||
ret0, _ := ret[0].(*api.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
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// WriteToFile mocks base method
|
||||
func (m *MockKubeConfig) WriteToFile(arg0 *api.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)
|
||||
}
|
||||
|
||||
// NewClientConfig mocks base method
|
||||
func (m *MockHTTP) NewClientConfig() interfaces.HTTPClientConfig {
|
||||
ret := m.ctrl.Call(m, "NewClientConfig")
|
||||
ret0, _ := ret[0].(interfaces.HTTPClientConfig)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// NewClientConfig indicates an expected call of NewClientConfig
|
||||
func (mr *MockHTTPMockRecorder) NewClientConfig() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientConfig", reflect.TypeOf((*MockHTTP)(nil).NewClientConfig))
|
||||
}
|
||||
|
||||
// MockHTTPClientConfig is a mock of HTTPClientConfig interface
|
||||
type MockHTTPClientConfig struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockHTTPClientConfigMockRecorder
|
||||
}
|
||||
|
||||
// MockHTTPClientConfigMockRecorder is the mock recorder for MockHTTPClientConfig
|
||||
type MockHTTPClientConfigMockRecorder struct {
|
||||
mock *MockHTTPClientConfig
|
||||
}
|
||||
|
||||
// NewMockHTTPClientConfig creates a new mock instance
|
||||
func NewMockHTTPClientConfig(ctrl *gomock.Controller) *MockHTTPClientConfig {
|
||||
mock := &MockHTTPClientConfig{ctrl: ctrl}
|
||||
mock.recorder = &MockHTTPClientConfigMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockHTTPClientConfig) EXPECT() *MockHTTPClientConfigMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddCertificateFromFile mocks base method
|
||||
func (m *MockHTTPClientConfig) AddCertificateFromFile(arg0 string) error {
|
||||
ret := m.ctrl.Call(m, "AddCertificateFromFile", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddCertificateFromFile indicates an expected call of AddCertificateFromFile
|
||||
func (mr *MockHTTPClientConfigMockRecorder) AddCertificateFromFile(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertificateFromFile", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddCertificateFromFile), arg0)
|
||||
}
|
||||
|
||||
// AddEncodedCertificate mocks base method
|
||||
func (m *MockHTTPClientConfig) AddEncodedCertificate(arg0 string) error {
|
||||
ret := m.ctrl.Call(m, "AddEncodedCertificate", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddEncodedCertificate indicates an expected call of AddEncodedCertificate
|
||||
func (mr *MockHTTPClientConfigMockRecorder) AddEncodedCertificate(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEncodedCertificate", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddEncodedCertificate), arg0)
|
||||
}
|
||||
|
||||
// SetSkipTLSVerify mocks base method
|
||||
func (m *MockHTTPClientConfig) SetSkipTLSVerify(arg0 bool) {
|
||||
m.ctrl.Call(m, "SetSkipTLSVerify", arg0)
|
||||
}
|
||||
|
||||
// SetSkipTLSVerify indicates an expected call of SetSkipTLSVerify
|
||||
func (mr *MockHTTPClientConfigMockRecorder) SetSkipTLSVerify(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSkipTLSVerify", reflect.TypeOf((*MockHTTPClientConfig)(nil).SetSkipTLSVerify), arg0)
|
||||
}
|
||||
|
||||
// TLSConfig mocks base method
|
||||
func (m *MockHTTPClientConfig) TLSConfig() *tls.Config {
|
||||
ret := m.ctrl.Call(m, "TLSConfig")
|
||||
ret0, _ := ret[0].(*tls.Config)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// TLSConfig indicates an expected call of TLSConfig
|
||||
func (mr *MockHTTPClientConfigMockRecorder) TLSConfig() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TLSConfig", reflect.TypeOf((*MockHTTPClientConfig)(nil).TLSConfig))
|
||||
// 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
|
||||
@@ -203,30 +84,128 @@ 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)
|
||||
}
|
||||
|
||||
// VerifyIDToken mocks base method
|
||||
func (m *MockOIDC) VerifyIDToken(arg0 context.Context, arg1 interfaces.OIDCVerifyTokenIn) (*go_oidc.IDToken, error) {
|
||||
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
|
||||
// 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 *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)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// VerifyIDToken indicates an expected call of VerifyIDToken
|
||||
func (mr *MockOIDCMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockOIDC)(nil).VerifyIDToken), arg0, arg1)
|
||||
// Verify indicates an expected call of Verify
|
||||
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
|
||||
@@ -253,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)
|
||||
@@ -268,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
|
||||
@@ -295,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.Issuer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
|
||||
}
|
||||
flow := oauth2cli.AuthCodeFlow{
|
||||
Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
Scopes: append(in.ExtraScopes, oidc.ScopeOpenID),
|
||||
},
|
||||
LocalServerPort: in.LocalServerPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
|
||||
ShowLocalServerURL: cb.ShowLocalServerURL,
|
||||
}
|
||||
token, err := flow.GetToken(ctx)
|
||||
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.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) VerifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) (*oidc.IDToken, error) {
|
||||
if in.Client != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
|
||||
}
|
||||
provider, err := oidc.NewProvider(ctx, in.Issuer)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
|
||||
}
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not verify the id_token")
|
||||
}
|
||||
return verifiedIDToken, nil
|
||||
}
|
||||
75
adaptors/oidc/http.go
Normal file
75
adaptors/oidc/http.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/logging"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func newHTTPClient(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
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 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.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")
|
||||
}
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
if len(pool.Subjects()) > 0 {
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
|
||||
return &http.Client{
|
||||
Transport: &logging.Transport{
|
||||
Base: &http.Transport{
|
||||
TLSClientConfig: &tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Logger: logger,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read %s", filename)
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return errors.Errorf("could not append certificate from %s", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not decode base64")
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return errors.Errorf("could not append certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
118
adaptors/oidc/oidc.go
Normal 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
|
||||
}
|
||||
@@ -1,38 +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 {
|
||||
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: "localhost:9000",
|
||||
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
|
||||
}
|
||||
|
||||
107
adaptors_test/authserver/code.go
Normal file
107
adaptors_test/authserver/code.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
98
adaptors_test/authserver/password.go
Normal file
98
adaptors_test/authserver/password.go
Normal 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
|
||||
}
|
||||
35
adaptors_test/authserver/templates.go
Normal file
35
adaptors_test/authserver/templates.go
Normal 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 }
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors_test/authserver"
|
||||
"github.com/int128/kubelogin/adaptors_test/keys"
|
||||
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
|
||||
@@ -19,150 +19,230 @@ import (
|
||||
|
||||
// Run the integration tests.
|
||||
//
|
||||
// 1. Start the auth server at port 9000.
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for port 8000.
|
||||
// 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:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
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: "http://localhost:9000",
|
||||
Issuer: codeConfig.Issuer,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
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",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("env:KUBECONFIG", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
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: codeConfig.Issuer,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: codeConfig.IDToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "http://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
Scope: "profile groups openid",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
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: "http://localhost:9000",
|
||||
Issuer: codeConfig.Issuer,
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
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("CACert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "https://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
TLSServerCert: keys.TLSServerCert,
|
||||
TLSServerKey: keys.TLSServerKey,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
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: "https://localhost:9000",
|
||||
Issuer: codeConfig.Issuer,
|
||||
IDPCertificateAuthority: keys.TLSCACert,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
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",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACertData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "https://localhost:9000")
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "https://localhost:9000",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
TLSServerCert: keys.TLSServerCert,
|
||||
TLSServerKey: keys.TLSServerKey,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
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: "https://localhost:9000",
|
||||
Issuer: codeConfig.Issuer,
|
||||
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
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",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
serverConfig := authserver.Config{
|
||||
Issuer: "http://localhost:9000",
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer server.Shutdown(ctx)
|
||||
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, "http://localhost:9000")
|
||||
idToken := newIDToken(t, codeConfig.Issuer)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: "http://localhost:9000",
|
||||
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,
|
||||
})
|
||||
@@ -171,11 +251,19 @@ func TestCmd_Run(t *testing.T) {
|
||||
|
||||
func newIDToken(t *testing.T, issuer string) string {
|
||||
t.Helper()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
})
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
}
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
@@ -183,40 +271,73 @@ 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, 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", "http://localhost:8000/", 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() {
|
||||
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)
|
||||
defer wg.Done()
|
||||
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)
|
||||
return &browserRequest{t, urlCh, &wg}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
4
adaptors_test/kubeconfig/testdata/dummy.yaml
vendored
Normal file
4
adaptors_test/kubeconfig/testdata/dummy.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
current-context: dummy
|
||||
kind: Config
|
||||
preferences: {}
|
||||
@@ -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 {
|
||||
|
||||
73
di/di.go
73
di/di.go
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
88
di/wire_gen.go
Normal 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)
|
||||
@@ -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
BIN
docs/keycloak-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
@@ -2,30 +2,37 @@
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have administrator access to the Keycloak.
|
||||
- You have the Cluster Admin role of the Kubernetes cluster.
|
||||
- You have an administrator role of the Keycloak realm.
|
||||
- You have an administrator role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed to your computer.
|
||||
- `kubectl` and `kubelogin` are installed.
|
||||
|
||||
## 1. Setup Keycloak
|
||||
|
||||
Open the Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Redirect URL: `http://localhost:8000/`
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
- Client ID: `kubernetes`
|
||||
- Groups claim: `groups`
|
||||
- Valid Redirect URLs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
|
||||
Create a group `kubernetes:admin` and join to it.
|
||||
This is used for group based access control.
|
||||
You can associate client roles by adding the following mapper:
|
||||
|
||||
- Name: `groups`
|
||||
- Mapper Type: `User Client Role`
|
||||
- Client ID: `kubernetes`
|
||||
- Client Role prefix: `kubernetes:`
|
||||
- Token Claim Name: `groups`
|
||||
- Add to ID token: on
|
||||
|
||||
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
|
||||
## 2. Setup Kubernetes API server
|
||||
|
||||
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:
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
@@ -50,7 +57,7 @@ roleRef:
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: Group
|
||||
name: /kubernetes:admin
|
||||
name: kubernetes:admin
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
@@ -60,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 \
|
||||
@@ -73,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:
|
||||
|
||||
17
go.mod
17
go.mod
@@ -2,27 +2,24 @@ 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.2.0
|
||||
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.2.1
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/int128/oauth2cli v1.4.0
|
||||
github.com/json-iterator/go v1.1.6 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
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/pflag v1.0.3 // 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-20190325154230-a5d413f7728c // indirect
|
||||
golang.org/x/net v0.0.0-20190328230028-74de082e2cca // indirect
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
|
||||
|
||||
74
go.sum
74
go.sum
@@ -1,37 +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.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
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/int128/oauth2cli v1.1.0 h1:qAT6C8GyaLaSf0aseQUTcJvZ+j2MueETzGkpoFow0kc=
|
||||
github.com/int128/oauth2cli v1.1.0/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
|
||||
github.com/int128/oauth2cli v1.2.1 h1:rhYQ++Kijz/sleAfzy2u2qEsQJCQSHVYjANgOM/LfLA=
|
||||
github.com/int128/oauth2cli v1.2.1/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/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=
|
||||
@@ -40,32 +59,55 @@ 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-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/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-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
|
||||
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// FindOIDCAuthProvider returns the current OIDC authProvider.
|
||||
// If the context, auth-info or auth-provider does not exist, this returns an error.
|
||||
// If auth-provider is not "oidc", this returns an error.
|
||||
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
|
||||
context := config.Contexts[config.CurrentContext]
|
||||
if context == nil {
|
||||
return nil, errors.Errorf("context %s does not exist", config.CurrentContext)
|
||||
}
|
||||
authInfo := config.AuthInfos[context.AuthInfo]
|
||||
if authInfo == nil {
|
||||
return nil, errors.Errorf("auth-info %s does not exist", context.AuthInfo)
|
||||
}
|
||||
if authInfo.AuthProvider == nil {
|
||||
return nil, errors.Errorf("auth-provider is not set")
|
||||
}
|
||||
if authInfo.AuthProvider.Name != "oidc" {
|
||||
return nil, errors.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
|
||||
}
|
||||
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
|
||||
}
|
||||
|
||||
// OIDCAuthProvider represents OIDC configuration in the kubeconfig.
|
||||
type OIDCAuthProvider api.AuthProviderConfig
|
||||
|
||||
// IDPIssuerURL returns the idp-issuer-url.
|
||||
func (c *OIDCAuthProvider) IDPIssuerURL() string {
|
||||
return c.Config["idp-issuer-url"]
|
||||
}
|
||||
|
||||
// ClientID returns the client-id.
|
||||
func (c *OIDCAuthProvider) ClientID() string {
|
||||
return c.Config["client-id"]
|
||||
}
|
||||
|
||||
// ClientSecret returns the client-secret.
|
||||
func (c *OIDCAuthProvider) ClientSecret() string {
|
||||
return c.Config["client-secret"]
|
||||
}
|
||||
|
||||
// IDPCertificateAuthority returns the idp-certificate-authority.
|
||||
func (c *OIDCAuthProvider) IDPCertificateAuthority() string {
|
||||
return c.Config["idp-certificate-authority"]
|
||||
}
|
||||
|
||||
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
|
||||
func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
|
||||
return c.Config["idp-certificate-authority-data"]
|
||||
}
|
||||
|
||||
// ExtraScopes returns the extra-scopes.
|
||||
func (c *OIDCAuthProvider) ExtraScopes() []string {
|
||||
if c.Config["extra-scopes"] == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(c.Config["extra-scopes"], ",")
|
||||
}
|
||||
|
||||
// IDToken returns the id-token.
|
||||
func (c *OIDCAuthProvider) IDToken() string {
|
||||
return c.Config["id-token"]
|
||||
}
|
||||
|
||||
// SetIDToken replaces the id-token.
|
||||
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
|
||||
c.Config["id-token"] = idToken
|
||||
}
|
||||
|
||||
// SetRefreshToken replaces the refresh-token.
|
||||
func (c *OIDCAuthProvider) SetRefreshToken(refreshToken string) {
|
||||
c.Config["refresh-token"] = refreshToken
|
||||
}
|
||||
@@ -3,7 +3,7 @@ class Kubelogin < Formula
|
||||
homepage "https://github.com/int128/kubelogin"
|
||||
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
|
||||
version "{{ env "VERSION" }}"
|
||||
sha256 "{{ .darwin_amd64_zip_sha256 }}"
|
||||
sha256 "{{ sha256 .darwin_amd64_archive }}"
|
||||
def install
|
||||
bin.install "kubelogin" => "kubelogin"
|
||||
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
|
||||
|
||||
8
main.go
8
main.go
@@ -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))
|
||||
}
|
||||
|
||||
27
models/kubeconfig/kubeconfig.go
Normal file
27
models/kubeconfig/kubeconfig.go
Normal 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
|
||||
}
|
||||
@@ -26,7 +26,7 @@ spec:
|
||||
version: {{ env "VERSION" }}
|
||||
platforms:
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
|
||||
sha256: "{{ .linux_amd64_zip_sha256 }}"
|
||||
sha256: "{{ sha256 .linux_amd64_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: "kubelogin"
|
||||
@@ -36,7 +36,7 @@ spec:
|
||||
os: linux
|
||||
arch: amd64
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip
|
||||
sha256: "{{ .darwin_amd64_zip_sha256 }}"
|
||||
sha256: "{{ sha256 .darwin_amd64_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: "kubelogin"
|
||||
@@ -46,7 +46,7 @@ spec:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_windows_amd64.zip
|
||||
sha256: "{{ .windows_amd64_zip_sha256 }}"
|
||||
sha256: "{{ sha256 .windows_amd64_archive }}"
|
||||
bin: kubelogin.exe
|
||||
files:
|
||||
- from: "kubelogin.exe"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package usecases
|
||||
|
||||
import "context"
|
||||
|
||||
//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 {
|
||||
KubeConfig string
|
||||
SkipTLSVerify bool
|
||||
SkipOpenBrowser bool
|
||||
ListenPort int
|
||||
}
|
||||
@@ -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 %[1]s \
|
||||
--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, e.g. token or password")
|
||||
|
||||
u.Logger.Debugf(1, "Loading %s", in.KubeConfig)
|
||||
cfg, err := u.KubeConfig.LoadFromFile(in.KubeConfig)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read the kubeconfig")
|
||||
}
|
||||
|
||||
u.Logger.Printf("Using current-context: %s", cfg.CurrentContext)
|
||||
authProvider, err := kubeconfig.FindOIDCAuthProvider(cfg)
|
||||
if err != nil {
|
||||
u.Logger.Printf(oidcConfigErrorMessage, cfg.CurrentContext)
|
||||
return errors.Wrapf(err, "could not find an oidc auth-provider in the kubeconfig")
|
||||
}
|
||||
|
||||
clientConfig := u.HTTP.NewClientConfig()
|
||||
clientConfig.SetSkipTLSVerify(in.SkipTLSVerify)
|
||||
if authProvider.IDPCertificateAuthority() != "" {
|
||||
filename := authProvider.IDPCertificateAuthority()
|
||||
u.Logger.Printf("Using the certificate %s", filename)
|
||||
if err := clientConfig.AddCertificateFromFile(filename); err != nil {
|
||||
u.Logger.Printf("Skip the certificate %s: %s", filename, err)
|
||||
}
|
||||
}
|
||||
if authProvider.IDPCertificateAuthorityData() != "" {
|
||||
encoded := authProvider.IDPCertificateAuthorityData()
|
||||
u.Logger.Printf("Using the certificate of idp-certificate-authority-data")
|
||||
if err := clientConfig.AddEncodedCertificate(encoded); err != nil {
|
||||
u.Logger.Printf("Skip the certificate of idp-certificate-authority-data: %s", err)
|
||||
}
|
||||
}
|
||||
hc, err := u.HTTP.NewClient(clientConfig)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not create a HTTP client")
|
||||
}
|
||||
|
||||
if token := u.verifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
|
||||
IDToken: authProvider.IDToken(),
|
||||
Issuer: authProvider.IDPIssuerURL(),
|
||||
ClientID: authProvider.ClientID(),
|
||||
Client: hc,
|
||||
}); token != nil {
|
||||
u.Logger.Printf("You already have a valid token (until %s)", token.Expiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
out, err := u.OIDC.Authenticate(ctx,
|
||||
adaptors.OIDCAuthenticateIn{
|
||||
Issuer: authProvider.IDPIssuerURL(),
|
||||
ClientID: authProvider.ClientID(),
|
||||
ClientSecret: authProvider.ClientSecret(),
|
||||
ExtraScopes: authProvider.ExtraScopes(),
|
||||
Client: hc,
|
||||
LocalServerPort: in.ListenPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
},
|
||||
adaptors.OIDCAuthenticateCallback{
|
||||
ShowLocalServerURL: func(url string) {
|
||||
u.Logger.Printf("Open %s for authentication", url)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get token from OIDC provider")
|
||||
}
|
||||
|
||||
u.Logger.Printf("Got a token for subject %s (valid until %s)", out.VerifiedIDToken.Subject, out.VerifiedIDToken.Expiry)
|
||||
u.Logger.Debugf(1, "Got an ID token %+v", out.VerifiedIDToken)
|
||||
authProvider.SetIDToken(out.IDToken)
|
||||
authProvider.SetRefreshToken(out.RefreshToken)
|
||||
|
||||
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", in.KubeConfig)
|
||||
if err := u.KubeConfig.WriteToFile(cfg, in.KubeConfig); err != nil {
|
||||
return errors.Wrapf(err, "could not update the kubeconfig")
|
||||
}
|
||||
u.Logger.Printf("Updated %s", in.KubeConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Login) verifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) *oidc.IDToken {
|
||||
if in.IDToken == "" {
|
||||
return nil
|
||||
}
|
||||
token, err := u.OIDC.VerifyIDToken(ctx, in)
|
||||
if err != nil {
|
||||
u.Logger.Debugf(1, "Could not verify the ID token in the kubeconfig: %s", err)
|
||||
return nil
|
||||
}
|
||||
u.Logger.Debugf(1, "Verified token %+v", token)
|
||||
return token
|
||||
}
|
||||
104
usecases/login/exec.go
Normal file
104
usecases/login/exec.go
Normal 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
|
||||
}
|
||||
98
usecases/login/exec_test.go
Normal file
98
usecases/login/exec_test.go
Normal 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
122
usecases/login/login.go
Normal 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)
|
||||
}
|
||||
264
usecases/login/login_test.go
Normal file
264
usecases/login/login_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,521 +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/usecases/interfaces"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
func TestLogin_Do(t *testing.T) {
|
||||
httpClient := &http.Client{}
|
||||
|
||||
newMockKubeConfig := func(ctrl *gomock.Controller, in *api.Config, out *api.Config) adaptors.KubeConfig {
|
||||
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
kubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/kubeconfig").
|
||||
Return(in, nil)
|
||||
kubeConfig.EXPECT().
|
||||
WriteToFile(out, "/path/to/kubeconfig")
|
||||
return kubeConfig
|
||||
}
|
||||
|
||||
newMockHTTP := func(ctrl *gomock.Controller, config adaptors.HTTPClientConfig) adaptors.HTTP {
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClientConfig().
|
||||
Return(config)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(config).
|
||||
Return(httpClient, nil)
|
||||
return mockHTTP
|
||||
}
|
||||
|
||||
newInConfig := func() *api.Config {
|
||||
return &api.Config{
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "default",
|
||||
Contexts: map[string]*api.Context{
|
||||
"default": {
|
||||
AuthInfo: "google",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"google": {
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: map[string]string{
|
||||
"client-id": "YOUR_CLIENT_ID",
|
||||
"client-secret": "YOUR_CLIENT_SECRET",
|
||||
"idp-issuer-url": "https://accounts.google.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
newOutConfig := func(in *api.Config) *api.Config {
|
||||
config := in.DeepCopy()
|
||||
config.AuthInfos["google"].AuthProvider.Config["id-token"] = "YOUR_ID_TOKEN"
|
||||
config.AuthInfos["google"].AuthProvider.Config["refresh-token"] = "YOUR_REFRESH_TOKEN"
|
||||
return config
|
||||
}
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
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)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SkipTLSVerify", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(true)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
SkipTLSVerify: true,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SkipOpenBrowser", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
SkipOpenBrowser: true,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
SkipOpenBrowser: true,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/ValidToken", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "VALID"
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
kubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
kubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/kubeconfig").
|
||||
Return(inConfig, nil)
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
|
||||
IDToken: "VALID",
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
Client: httpClient,
|
||||
}).
|
||||
Return(&oidc.IDToken{}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: kubeConfig,
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/InvalidToken", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["id-token"] = "EXPIRED"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
VerifyIDToken(ctx, adaptors.OIDCVerifyTokenIn{
|
||||
IDToken: "EXPIRED",
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
Client: httpClient,
|
||||
}).
|
||||
Return(nil, errors.New("token is expired"))
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/extra-scopes", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["extra-scopes"] = "email,profile"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/idp-certificate-authority", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority"] = "/path/to/cert"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
httpClientConfig.EXPECT().
|
||||
AddCertificateFromFile("/path/to/cert")
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/idp-certificate-authority/error", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority"] = "/path/to/cert"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
httpClientConfig.EXPECT().
|
||||
AddCertificateFromFile("/path/to/cert").
|
||||
Return(errors.New("not found"))
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/idp-certificate-authority-data", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority-data"] = "base64encoded"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
httpClientConfig.EXPECT().
|
||||
AddEncodedCertificate("base64encoded")
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/idp-certificate-authority-data/error", func(t *testing.T) {
|
||||
inConfig := newInConfig()
|
||||
inConfig.AuthInfos["google"].AuthProvider.Config["idp-certificate-authority-data"] = "base64encoded"
|
||||
outConfig := newOutConfig(inConfig)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
httpClientConfig := mock_adaptors.NewMockHTTPClientConfig(ctrl)
|
||||
httpClientConfig.EXPECT().
|
||||
SetSkipTLSVerify(false)
|
||||
httpClientConfig.EXPECT().
|
||||
AddEncodedCertificate("base64encoded").
|
||||
Return(errors.New("invalid"))
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, adaptors.OIDCAuthenticateIn{
|
||||
Issuer: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{},
|
||||
LocalServerPort: 10000,
|
||||
Client: httpClient,
|
||||
}, gomock.Any()).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: newMockKubeConfig(ctrl, inConfig, outConfig),
|
||||
HTTP: newMockHTTP(ctrl, httpClientConfig),
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfig: "/path/to/kubeconfig",
|
||||
ListenPort: 10000,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
45
usecases/usecases.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user