mirror of
https://github.com/int128/kubelogin.git
synced 2026-03-02 17:00:20 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8967faf6b | ||
|
|
315d6151d7 | ||
|
|
1ff03fdfb3 | ||
|
|
5e0fc7f399 | ||
|
|
9423a65f46 | ||
|
|
45417a18fd | ||
|
|
760416fd04 | ||
|
|
0a4ebb26c2 | ||
|
|
de9f7a2a01 | ||
|
|
0006cdda2d | ||
|
|
c89a8a1823 | ||
|
|
4f566a7b32 | ||
|
|
5158159bdd | ||
|
|
3a2aa0c6c0 | ||
|
|
56b17efae1 | ||
|
|
3e5be43d8a | ||
|
|
1ffa927432 | ||
|
|
5c6b461f37 |
@@ -24,10 +24,6 @@ As well as it respects the environment variable `KUBECONFIG`.
|
||||
|
||||
TODO
|
||||
|
||||
### Wrap kubectl and login transparently
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
7
Makefile
7
Makefile
@@ -3,13 +3,11 @@ TARGET_PLUGIN := kubectl-oidc_login
|
||||
CIRCLE_TAG ?= HEAD
|
||||
LDFLAGS := -X main.version=$(CIRCLE_TAG)
|
||||
|
||||
.PHONY: check run diagram release clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
golangci-lint run
|
||||
$(MAKE) -C e2e_test/keys/testdata
|
||||
go test -v -race -cover -coverprofile=coverage.out ./...
|
||||
|
||||
$(TARGET): $(wildcard *.go)
|
||||
@@ -18,6 +16,7 @@ $(TARGET): $(wildcard *.go)
|
||||
$(TARGET_PLUGIN): $(TARGET)
|
||||
ln -sf $(TARGET) $@
|
||||
|
||||
.PHONY: run
|
||||
run: $(TARGET_PLUGIN)
|
||||
-PATH=.:$(PATH) kubectl oidc-login --help
|
||||
|
||||
@@ -32,11 +31,13 @@ dist:
|
||||
mkdir -p dist/plugins
|
||||
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
|
||||
|
||||
.PHONY: release
|
||||
release: dist
|
||||
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
|
||||
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
|
||||
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(CIRCLE_TAG)" -m "Bump oidc-login to $(CIRCLE_TAG)" -C dist/ plugins/oidc-login.yaml
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm $(TARGET)
|
||||
-rm $(TARGET_PLUGIN)
|
||||
|
||||
98
README.md
98
README.md
@@ -2,8 +2,9 @@
|
||||
|
||||
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`.
|
||||
|
||||
In Kubernetes OIDC authentication, kubectl does not provide actual authentication and we need to manually set an ID token and refresh token to the kubeconfig.
|
||||
Kubelogin integrates browser based authentication with kubectl.
|
||||
This is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
When you run `kubectl`, kubelogin opens the browser and you can log in to the provider.
|
||||
Then kubelogin gets a token from the provider and kubectl calls the Kubernetes APIs with the token.
|
||||
|
||||
|
||||
## Getting Started
|
||||
@@ -19,7 +20,7 @@ brew install kubelogin
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.13.0/kubelogin_linux_amd64.zip
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.1/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
@@ -28,19 +29,16 @@ You need to configure the OIDC provider, Kubernetes API server, kubeconfig and r
|
||||
See the following documents for more:
|
||||
|
||||
- [Getting Started with Keycloak](docs/keycloak.md)
|
||||
- [Getting Started with dex and GitHub](docs/dex.md)
|
||||
- [Getting Started with Google Identity Platform](docs/google.md)
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
You can run kubelogin as the following methods:
|
||||
|
||||
- Run as a credential plugin
|
||||
- Run as a standalone command
|
||||
- Wrap kubectl (deprecated)
|
||||
- Credential plugin mode
|
||||
- Standalone mode
|
||||
|
||||
|
||||
### Run as a credential plugin
|
||||
|
||||
Status: beta since kubelogin v1.14.0.
|
||||
### Credential plugin mode
|
||||
|
||||
You can run kubelogin as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
This provides transparent login without manually running `kubelogin` command.
|
||||
@@ -53,8 +51,9 @@ users:
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://issuer.example.com
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
@@ -82,16 +81,17 @@ NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
Kubelogin writes the ID token and refresh token to the cache file.
|
||||
Kubelogin writes the ID token and refresh token to the token cache file.
|
||||
|
||||
If the cached ID token is valid, kubelogin just returns it.
|
||||
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
If the refresh token has expired, kubelogin will perform reauthentication.
|
||||
|
||||
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
|
||||
Kubelogin will perform authentication if the token cache file does not exist.
|
||||
|
||||
|
||||
### Run as a standalone command
|
||||
|
||||
Status: stable.
|
||||
### Standalone mode
|
||||
|
||||
You can run kubelogin as a standalone command.
|
||||
In this method, you need to manually run the command before running kubectl.
|
||||
@@ -131,7 +131,7 @@ You got a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now you can access to the cluster.
|
||||
Now you can access the cluster.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
@@ -139,6 +139,22 @@ NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
Your kubeconfig looks like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: YOUR_CLIENT_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: https://issuer.example.com
|
||||
id-token: ey... # kubelogin will add or update the ID token here
|
||||
refresh-token: ey... # kubelogin will add or update the refresh token here
|
||||
name: oidc
|
||||
```
|
||||
|
||||
If the ID token is valid, kubelogin does nothing.
|
||||
|
||||
```
|
||||
@@ -150,54 +166,13 @@ If the ID token has expired, kubelogin will refresh the token using the refresh
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
|
||||
### Wrap kubectl
|
||||
|
||||
Status: DEPRECATED and will be removed in kubelogin v1.15.0.
|
||||
|
||||
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, kubelogin 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 ID token is valid, kubelogin just executes kubectl.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
Kubelogin 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`, kubelogin just executes kubectl.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
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).
|
||||
|
||||
|
||||
### Run as a credential plugin
|
||||
### Credential plugin mode
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
@@ -220,7 +195,7 @@ Flags:
|
||||
--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
|
||||
--token-cache string Path to a file for caching the token (default "~/.kube/oidc-login.token-cache")
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
-h, --help help for get-token
|
||||
```
|
||||
|
||||
@@ -242,7 +217,7 @@ You can use your self-signed certificates for the provider.
|
||||
```
|
||||
|
||||
|
||||
### Run as a standalone command
|
||||
### Standalone mode
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
@@ -265,7 +240,6 @@ Examples:
|
||||
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
|
||||
|
||||
Available Commands:
|
||||
exec Login transparently and execute the kubectl command (deprecated)
|
||||
get-token Run as a kubectl credential plugin
|
||||
help Help about any command
|
||||
version Print the version information
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
getToken := mock_usecases.NewMockGetToken(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, usecases.GetTokenIn{
|
||||
ListenPort: defaultListenPort,
|
||||
TokenCacheFilename: defaultTokenCache,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
getToken := mock_usecases.NewMockGetToken(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, usecases.GetTokenIn{
|
||||
TokenCacheFilename: defaultTokenCache,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
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{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--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("get-token/MissingMandatoryOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
GetToken: mock_usecases.NewMockGetToken(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
GetToken: mock_usecases.NewMockGetToken(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package tokencache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Repository), "*"),
|
||||
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
|
||||
)
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
func (*Repository) Read(filename string) (*credentialplugin.TokenCache, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
var c credentialplugin.TokenCache
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (*Repository) Write(filename string, tc credentialplugin.TokenCache) error {
|
||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not create file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
e := json.NewEncoder(f)
|
||||
if err := e.Encode(&tc); err != nil {
|
||||
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
141
docs/dex.md
Normal file
141
docs/dex.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Getting Started with dex and GitHub
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have a GitHub account.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed.
|
||||
|
||||
## 1. Setup GitHub OAuth
|
||||
|
||||
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
|
||||
|
||||
- Application name: (any)
|
||||
- Homepage URL: `https://dex.example.com`
|
||||
- Authorization callback URL: `https://dex.example.com/callback`
|
||||
|
||||
## 2. Setup dex
|
||||
|
||||
Configure the dex with the following config:
|
||||
|
||||
```yaml
|
||||
issuer: https://dex.example.com
|
||||
connectors:
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: YOUR_GITHUB_CLIENT_ID
|
||||
clientSecret: YOUR_GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://dex.example.com/callback
|
||||
staticClients:
|
||||
- id: kubernetes
|
||||
name: Kubernetes
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
secret: YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Now test authentication with the dex.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
--oidc-issuer-url=https://dex.example.com \
|
||||
--oidc-client-id=kubernetes \
|
||||
--oidc-client-secret=YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
|
||||
```
|
||||
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://dex.example.com
|
||||
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
|
||||
17:21:32.052683 get_token.go:57: ID token has the claim: aud=kubernetes
|
||||
```
|
||||
|
||||
## 3. 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).
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://dex.example.com
|
||||
--oidc-client-id=kubernetes
|
||||
```
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://dex.example.com
|
||||
oidcClientID: kubernetes
|
||||
```
|
||||
|
||||
## 4. Create a role binding
|
||||
|
||||
Here assign the `cluster-admin` role to your subject.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: keycloak-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 5. Setup kubeconfig
|
||||
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.example.com
|
||||
name: example.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example.k8s.local
|
||||
user: dex
|
||||
name: dex@example.k8s.local
|
||||
current-context: dex@example.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: dex
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://dex.example.com
|
||||
- --oidc-client-id=kubernetes
|
||||
- --oidc-client-secret=YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 6. Run kubectl
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
|
||||
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
|
||||
```
|
||||
@@ -13,6 +13,23 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
|
||||
|
||||
- Application Type: Other
|
||||
|
||||
Now test authentication with Google Identity Platform.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
--oidc-issuer-url=https://accounts.google.com \
|
||||
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
|
||||
```
|
||||
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://accounts.google.com
|
||||
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
|
||||
17:21:32.052683 get_token.go:57: ID token has the claim: aud=YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
```
|
||||
|
||||
## 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).
|
||||
@@ -33,7 +50,7 @@ spec:
|
||||
|
||||
## 3. Setup Kubernetes cluster
|
||||
|
||||
Here assign the `cluster-admin` role to you.
|
||||
Here assign the `cluster-admin` role to your subject.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
@@ -46,7 +63,7 @@ roleRef:
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: https://accounts.google.com#1234567890
|
||||
name: YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
@@ -56,6 +73,19 @@ You can create a custom role and assign it as well.
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.example.com
|
||||
name: example.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example.k8s.local
|
||||
user: google
|
||||
name: google@example.k8s.local
|
||||
current-context: google@example.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: google
|
||||
user:
|
||||
@@ -69,9 +99,11 @@ users:
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
|
||||
@@ -28,6 +28,24 @@ You can associate client roles by adding the following mapper:
|
||||
|
||||
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
|
||||
Now test authentication with the Keycloak.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
|
||||
--oidc-client-id=kubernetes \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
|
||||
```
|
||||
17:21:32.052655 get_token.go:57: ID token has the claim: iss=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
17:21:32.052672 get_token.go:57: ID token has the claim: sub=YOUR_SUBJECT
|
||||
17:21:32.052683 get_token.go:57: ID token has the claim: aud=kubernetes
|
||||
17:21:32.052694 get_token.go:57: ID token has 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).
|
||||
@@ -73,6 +91,19 @@ You can create a custom role and assign it as well.
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.example.com
|
||||
name: example.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example.k8s.local
|
||||
user: keycloak
|
||||
name: keycloak@example.k8s.local
|
||||
current-context: keycloak@example.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
@@ -86,9 +117,11 @@ users:
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# Team on-boarding
|
||||
|
||||
## kops
|
||||
|
||||
Export the kubeconfig.
|
||||
|
||||
```sh
|
||||
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
|
||||
```
|
||||
|
||||
Remove the `admin` access from the kubeconfig.
|
||||
It should look as like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: LS...
|
||||
server: https://api.hello.k8s.example.com
|
||||
name: hello.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: hello.k8s.local
|
||||
name: hello.k8s.local
|
||||
current-context: hello.k8s.local
|
||||
preferences: {}
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
args:
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
@@ -2,19 +2,21 @@ package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
@@ -26,6 +28,15 @@ import (
|
||||
//
|
||||
func TestCmd_Run_CredentialPlugin(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
cacheDir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a cache dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(cacheDir); err != nil {
|
||||
t.Errorf("could not clean up the cache dir: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -56,7 +67,7 @@ func TestCmd_Run_CredentialPlugin(t *testing.T) {
|
||||
runGetTokenCmd(t, ctx, req, credentialPluginInteraction,
|
||||
"--skip-open-browser",
|
||||
"--listen-port", "0",
|
||||
"--token-cache", "/dev/null",
|
||||
"--token-cache-dir", cacheDir,
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
)
|
||||
|
||||
3
e2e_test/keys/testdata/.gitignore
vendored
3
e2e_test/keys/testdata/.gitignore
vendored
@@ -1,4 +1 @@
|
||||
/CA
|
||||
*.key
|
||||
*.csr
|
||||
*.crt
|
||||
|
||||
9
e2e_test/keys/testdata/Makefile
vendored
9
e2e_test/keys/testdata/Makefile
vendored
@@ -1,13 +1,13 @@
|
||||
all: ca.key ca.crt server.key server.crt jws.key
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
all: server.crt ca.crt jws.key
|
||||
|
||||
clean:
|
||||
rm -v ca.* server.*
|
||||
-rm -v ca.* server.* jws.*
|
||||
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.INTERMEDIATE: ca.csr
|
||||
ca.csr: openssl.cnf ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
@@ -26,6 +26,7 @@ ca.crt: ca.csr ca.key
|
||||
server.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.INTERMEDIATE: server.csr
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
|
||||
11
e2e_test/keys/testdata/ca.crt
vendored
Normal file
11
e2e_test/keys/testdata/ca.crt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnTCCAQYCCQCuPrhkr+BvGzANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
|
||||
ZWxsbyBDQTAeFw0xOTA4MTgwNjAwMDZaFw0xOTA5MTcwNjAwMDZaMBMxETAPBgNV
|
||||
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
|
||||
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
|
||||
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
|
||||
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
|
||||
ACfgNePlOLnLz1zJrWN6RZ6q0a+SSK8HdgSiKSF66SBIRILFoQmapBLXRY9YyATt
|
||||
cdgg7pOd1WGCMqlOnhL56c8X5n+j/LGM5hc9PaEJA5vru7EBrnbxCkg0n8yp4Swc
|
||||
8KFV5IiZ5D8t03AHjrXLQg8/HRzTuFRJJ1nJmc+FbnjT
|
||||
-----END CERTIFICATE-----
|
||||
15
e2e_test/keys/testdata/ca.key
vendored
Normal file
15
e2e_test/keys/testdata/ca.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDnSTDsRx4UJmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ
|
||||
4Pc51itOycWN1nclNVb489i9J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3L
|
||||
u4mysxyOtYZNsdST8GBCP0m42tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQAB
|
||||
AoGAaYmTYm29QvKW4et9oPxDjpYG0bqlz7P0xFRR9kKtKTATAMHjWeu2xFR/JI+b
|
||||
rvJLIdZqHmWe5AmMb3NxZgfLonEB71ohaKQha1L8Vc7aoedRheJvqqaNr+ZxoCMO
|
||||
8xcjsaMYxLEVt0tg6XyKyEhi1/hOufFZ4BSng4oQbrpaNIkCQQD6hPEzzPZtMEEe
|
||||
eRdwTVUIStKFMQbRdwZ5Oc7pyDk2U+SFRJiqkBkqnmFekcf2UgbBQxem+GMhWNgE
|
||||
LItKy/wVAkEA7FiHxbzn2msaE3hZCWudtnXqmJNuPO0zJ5icXe2svwmwPfLA/rm9
|
||||
iazCuyzyK67J8IG9QgIjQFYXtQbMr2chGwJBAMm3dghBx0LQEf8Zfdf9TLSqmqyI
|
||||
d3b+IgZGl+cCQ58NGfp863ibIsiAUuK0+4/JKItBHLBjXF6jjPx/aYFGkqkCQH4w
|
||||
GnXCEYx1qJuCow87jR4xQQsrlC0lfC2E9t/TmWr6UkYRCWg3ZXJPcj0bl0Upcppd
|
||||
ut22ZHniPZAizEBOcMcCQQDnMEOxufxhMsx2NC8yON/noewLINqKcbkMsl6DjvJl
|
||||
+wLbQmzJ0j+uIBgdpsj4rWnEr7GxoL2eWG44QDUBco79
|
||||
-----END RSA PRIVATE KEY-----
|
||||
15
e2e_test/keys/testdata/jws.key
vendored
Normal file
15
e2e_test/keys/testdata/jws.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXwIBAAKBgQCZukkN1GxMlNXkpOZxCnvCF874/rn1sNKzO98fOwmBRPG2+m/c
|
||||
yqBY7t2L2nihqz3+GZTiHmzSrBzMAGPVW1qGmk9KYg3m7akz9SiCxdoUkgM9MCCp
|
||||
X/s8IhgtXkyoKFPcGdwHblDl/3aJG02b6TAQD8vTNQAKoKw7L0FST+pvRwIDAQAB
|
||||
AoGBAJT1fXR5MbfDQL+dSe6fSex5RYTgzzDTdldW3I1Wl487Tz0OzvYTIe0LCIJL
|
||||
4DhHxnpCL5IsCSbav8ytVA+ZxczHpEW6UxbalXt5UfgFu0joTrdoGxDcVWgUCW3J
|
||||
Olbln0lOP55wViKh509gt45Za3VxJrNul3khVfVj7qGG9cKBAkEAxwtT8LxwqTYF
|
||||
nqoeZvPp15JAqlgdk38ttJa4KEqvpBTSxNIXkL9T5gJ+irKZAzxlz/U7bhn5mw6E
|
||||
3xFiljOXpQJBAMW3XRFOjgNBXNjbt81wREF5LdZl9EI8cRMSH6xljt2uwSqw4EG3
|
||||
76gFvccUd+WnfspFQZVypSSD4pWzsAqh13sCQQCA9BLW5Y7r4ab0a2y08JNwaT1h
|
||||
3yKSO5QF6pu25uQyHpeKkj5YNcyKONV40EqXsRqZB10QcN2omlh1GJNRkm1NAkEA
|
||||
qV3lr4mnRUqcinfM/4MINT3k8h/sGUFFa5y+3SMyOtwURMm3kRRLi5c/dmYmPug4
|
||||
SHUDNU48AQeo9awzRShWOQJBAJdw+cfRgi4fo3HY33uZdFa1T9G+qTA2ijhco6O3
|
||||
8tOc0yOFEtPNXM87MsHGIQP3ZCLfIY1gs2O3WCTFbPxR4rc=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
2
e2e_test/keys/testdata/openssl.cnf
vendored
2
e2e_test/keys/testdata/openssl.cnf
vendored
@@ -10,7 +10,7 @@ new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
default_days = 365
|
||||
default_days = 3650
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
|
||||
52
e2e_test/keys/testdata/server.crt
vendored
Normal file
52
e2e_test/keys/testdata/server.crt
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 0 (0x0)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN=Hello CA
|
||||
Validity
|
||||
Not Before: Aug 18 06:00:06 2019 GMT
|
||||
Not After : Aug 15 06:00:06 2029 GMT
|
||||
Subject: CN=localhost
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:d6:4e:eb:3a:cb:25:f9:7e:92:22:f2:63:99:da:
|
||||
08:05:8b:a3:e7:d3:fd:71:3e:bd:da:c5:d5:63:b7:
|
||||
d3:7b:f8:cd:1a:2e:5c:a2:4f:48:98:c2:b4:da:e8:
|
||||
1e:d3:d7:8f:d8:ee:a9:70:d0:9d:4f:f4:8d:95:e5:
|
||||
8e:9a:71:b6:80:aa:0b:cb:28:1d:f6:0d:7e:aa:78:
|
||||
bf:30:e6:58:d7:6b:92:8f:19:1c:7d:95:f8:d5:2f:
|
||||
8c:58:49:98:88:05:50:88:80:a9:77:c4:16:b4:c1:
|
||||
00:45:1e:d3:d0:ed:98:4d:f7:a3:5d:f1:82:cb:a5:
|
||||
4d:19:64:4d:43:db:13:d4:17
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Non Repudiation, Key Encipherment
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:localhost
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
5a:5c:5e:8b:de:82:86:f4:98:40:0e:cf:c5:51:fe:89:46:49:
|
||||
f0:26:d2:a5:06:e3:91:43:c1:f8:b2:ad:b7:a1:23:13:1a:80:
|
||||
45:00:51:70:b6:06:63:c6:a8:c8:22:5d:1b:00:e0:4a:8c:2e:
|
||||
ce:b4:da:b1:89:8a:d2:d0:e3:eb:0f:16:34:45:a1:bd:64:5c:
|
||||
48:41:8c:0a:bf:66:be:1c:a8:35:47:ce:b0:dc:c8:4f:5e:c1:
|
||||
ec:ef:21:fb:45:55:95:e3:99:40:46:0b:6c:8a:b3:d5:f0:bf:
|
||||
39:a4:ba:c4:d7:58:88:58:08:07:98:59:6e:ca:9c:08:e4:c4:
|
||||
4f:db
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBzTCCATagAwIBAgIBADANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhIZWxs
|
||||
byBDQTAeFw0xOTA4MTgwNjAwMDZaFw0yOTA4MTUwNjAwMDZaMBQxEjAQBgNVBAMM
|
||||
CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1k7rOssl+X6S
|
||||
IvJjmdoIBYuj59P9cT692sXVY7fTe/jNGi5cok9ImMK02uge09eP2O6pcNCdT/SN
|
||||
leWOmnG2gKoLyygd9g1+qni/MOZY12uSjxkcfZX41S+MWEmYiAVQiICpd8QWtMEA
|
||||
RR7T0O2YTfejXfGCy6VNGWRNQ9sT1BcCAwEAAaMwMC4wCQYDVR0TBAIwADALBgNV
|
||||
HQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4GB
|
||||
AFpcXovegob0mEAOz8VR/olGSfAm0qUG45FDwfiyrbehIxMagEUAUXC2BmPGqMgi
|
||||
XRsA4EqMLs602rGJitLQ4+sPFjRFob1kXEhBjAq/Zr4cqDVHzrDcyE9ewezvIftF
|
||||
VZXjmUBGC2yKs9XwvzmkusTXWIhYCAeYWW7KnAjkxE/b
|
||||
-----END CERTIFICATE-----
|
||||
15
e2e_test/keys/testdata/server.key
vendored
Normal file
15
e2e_test/keys/testdata/server.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXgIBAAKBgQDWTus6yyX5fpIi8mOZ2ggFi6Pn0/1xPr3axdVjt9N7+M0aLlyi
|
||||
T0iYwrTa6B7T14/Y7qlw0J1P9I2V5Y6acbaAqgvLKB32DX6qeL8w5ljXa5KPGRx9
|
||||
lfjVL4xYSZiIBVCIgKl3xBa0wQBFHtPQ7ZhN96Nd8YLLpU0ZZE1D2xPUFwIDAQAB
|
||||
AoGBAJhNR7Dl1JwFzndViWE6aP7/6UEFEBWeADDs7aTLbFmrTJ+xmRWkgLRHk14L
|
||||
HnVwuYLywaoyJ8o9wy1nEbxC2e4zWZ94d351MQf3/komCXDBzEsktfAcNsAFnMmS
|
||||
HZuGXfhi0FYWoftpIGxUmEBmQRcq0ctycbLves6TY3y+oajpAkEA8UHmSr/zsM3E
|
||||
XQXPp2BCAvRrTH/njk4R0jwB29Bi89gt/XDD4uvfWbHw7TZxnZuCpWisnxpMPIwa
|
||||
1rjqIQmhEwJBAONncQUOxwYCIuvraIhV0QtkIUa+YpTvAxP8ZNXx+agtHmHG2TTf
|
||||
kGv2YddvjxXZItN/FZOzUGm9OptaeLRTpW0CQHO8CEzNnoqve0agtgf2LlSaiiqt
|
||||
pRhoLTZsYPvhEMcnapCNGvtt6bxul0REfOZ9poPRHhZJGE9naqydEnv80Y8CQQC3
|
||||
pxLfws95SsBpR/VkJepuCK/XMmrrXRxfR7coEgROjiG7VZyV1vgMOS9Ljg1A19wI
|
||||
cto6LtcCjpCGZsqU1/kBAkEAv2tXBts3vuIjguZNMz7KLWmu3zG2SQRaqdEZwL+R
|
||||
DQmD5tbI6gEtd5OmgmSiW8A4mpfgFYvG7Um2fwi7TTXtSA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,7 +1,7 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
)
|
||||
|
||||
func New(t testingLogger) *logger.Logger {
|
||||
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,56 +36,179 @@ var (
|
||||
func TestCmd_Run_Login(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
type testParameter struct {
|
||||
startServer func(t *testing.T, h http.Handler) (string, localserver.Shutdowner)
|
||||
kubeconfigIDPCertificateAuthority string
|
||||
clientTLSConfig *tls.Config
|
||||
}
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
testParameters := map[string]testParameter{
|
||||
"NoTLS": {
|
||||
startServer: localserver.Start,
|
||||
},
|
||||
"CACert": {
|
||||
startServer: func(t *testing.T, h http.Handler) (string, localserver.Shutdowner) {
|
||||
return localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, h)
|
||||
},
|
||||
kubeconfigIDPCertificateAuthority: keys.TLSCACert,
|
||||
clientTLSConfig: keys.TLSCACertAsConfig,
|
||||
},
|
||||
}
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
runTest := func(t *testing.T, p testParameter) {
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
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,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, p.clientTLSConfig)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
|
||||
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
|
||||
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: p.kubeconfigIDPCertificateAuthority,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, p.clientTLSConfig)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
for name, p := range testParameters {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
runTest(t, p)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("env:KUBECONFIG", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -141,151 +264,6 @@ func TestCmd_Run_Login(t *testing.T) {
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: keys.TLSCACert,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
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,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACertData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
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,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
|
||||
|
||||
1
go.mod
1
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/google/wire v0.3.0
|
||||
github.com/int128/oauth2cli v1.4.1
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/pflag v1.0.3
|
||||
|
||||
2
main.go
2
main.go
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
)
|
||||
|
||||
var version = "HEAD"
|
||||
|
||||
@@ -3,26 +3,27 @@ kind: Plugin
|
||||
metadata:
|
||||
name: oidc-login
|
||||
spec:
|
||||
shortDescription: Login for OpenID Connect authentication
|
||||
homepage: https://github.com/int128/kubelogin
|
||||
shortDescription: Log in to the OpenID Connect provider
|
||||
description: |
|
||||
This plugin gets a token from the OIDC provider and writes it to the kubeconfig.
|
||||
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
|
||||
|
||||
Just run:
|
||||
% kubectl oidc-login
|
||||
## Credential plugin mode
|
||||
kubectl executes oidc-login before calling the Kubernetes APIs.
|
||||
oidc-login automatically opens the browser and you can log in to the provider.
|
||||
After authentication, kubectl gets the token from oidc-login and you can access the cluster.
|
||||
See https://github.com/int128/kubelogin#credential-plugin-mode for more.
|
||||
|
||||
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.
|
||||
## Standalone mode
|
||||
Run `kubectl oidc-login`.
|
||||
It automatically opens the browser and you can log in to the provider.
|
||||
After authentication, it writes the token to the kubeconfig and you can access the cluster.
|
||||
See https://github.com/int128/kubelogin#standalone-mode for more.
|
||||
|
||||
caveats: |
|
||||
You need to setup the following components:
|
||||
* OIDC provider
|
||||
* Kubernetes API server
|
||||
* Role for your group or user
|
||||
* kubectl authentication
|
||||
|
||||
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
homepage: https://github.com/int128/kubelogin
|
||||
version: {{ env "VERSION" }}
|
||||
platforms:
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -31,108 +31,29 @@ const examples = ` # Login to the provider using the authorization code flow.
|
||||
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
|
||||
|
||||
var defaultListenPort = []int{8000, 18000}
|
||||
var defaultTokenCache = homedir.HomeDir() + "/.kube/oidc-login.token-cache"
|
||||
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
|
||||
|
||||
// Cmd provides interaction with command line interface (CLI).
|
||||
type Cmd struct {
|
||||
Login usecases.Login
|
||||
GetToken usecases.GetToken
|
||||
LoginAndExec usecases.LoginAndExec
|
||||
Logger adaptors.Logger
|
||||
Login usecases.Login
|
||||
GetToken usecases.GetToken
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
// Run parses the command line arguments and executes the specified use-case.
|
||||
// It returns an exit code, that is 0 on success or 1 on error.
|
||||
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())
|
||||
|
||||
//TODO: deprecated
|
||||
execCmd := cobra.Command{
|
||||
Use: "exec [flags] -- kubectl [args]",
|
||||
Short: "Login transparently and execute the kubectl command (deprecated)",
|
||||
Args: func(execCmd *cobra.Command, args []string) error {
|
||||
if execCmd.ArgsLenAtDash() == -1 {
|
||||
return xerrors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
|
||||
}
|
||||
if len(args) < 1 {
|
||||
return xerrors.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)
|
||||
rootCmd := newRootCmd(ctx, executable, cmd)
|
||||
rootCmd.Version = version
|
||||
rootCmd.SilenceUsage = true
|
||||
rootCmd.SilenceErrors = true
|
||||
|
||||
getTokenCmd := newGetTokenCmd(ctx, cmd)
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
@@ -140,14 +61,15 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
cmd.Logger.Printf("%s version %s", executable, version)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(&versionCmd)
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
rootCmd.SetArgs(args[1:])
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
cmd.Logger.Debugf(1, "error while parsing the arguments: %s", err)
|
||||
cmd.Logger.Printf("error: %s", err)
|
||||
cmd.Logger.Debugf(1, "stacktrace: %+v", err)
|
||||
return 1
|
||||
}
|
||||
return exitCode
|
||||
return 0
|
||||
}
|
||||
|
||||
// kubectlOptions represents kubectl specific options.
|
||||
@@ -170,15 +92,15 @@ func (o *kubectlOptions) register(f *pflag.FlagSet) {
|
||||
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
|
||||
}
|
||||
|
||||
// kubeloginOptions represents application specific options.
|
||||
type kubeloginOptions struct {
|
||||
// loginOptions represents the options for Login use-case.
|
||||
type loginOptions struct {
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (o *kubeloginOptions) register(f *pflag.FlagSet) {
|
||||
func (o *loginOptions) 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")
|
||||
@@ -186,9 +108,43 @@ func (o *kubeloginOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
|
||||
}
|
||||
|
||||
func newRootCmd(ctx context.Context, executable string, cmd *Cmd) *cobra.Command {
|
||||
var o struct {
|
||||
kubectlOptions
|
||||
loginOptions
|
||||
}
|
||||
rootCmd := &cobra.Command{
|
||||
Use: executable,
|
||||
Short: "Login to the OpenID Connect provider and update the kubeconfig",
|
||||
Example: fmt.Sprintf(examples, executable),
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
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 {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.kubectlOptions.register(rootCmd.Flags())
|
||||
o.loginOptions.register(rootCmd.Flags())
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// getTokenOptions represents the options for get-token command.
|
||||
type getTokenOptions struct {
|
||||
kubeloginOptions
|
||||
loginOptions
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
@@ -196,12 +152,12 @@ type getTokenOptions struct {
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
Verbose int
|
||||
TokenCacheFilename string
|
||||
TokenCacheDir string
|
||||
}
|
||||
|
||||
func (o *getTokenOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
o.kubeloginOptions.register(f)
|
||||
o.loginOptions.register(f)
|
||||
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
@@ -209,7 +165,7 @@ func (o *getTokenOptions) register(f *pflag.FlagSet) {
|
||||
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")
|
||||
f.StringVar(&o.TokenCacheFilename, "token-cache", defaultTokenCache, "Path to a file for caching the token")
|
||||
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
|
||||
}
|
||||
|
||||
func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
|
||||
@@ -232,17 +188,17 @@ func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
TokenCacheFilename: o.TokenCacheFilename,
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
TokenCacheDir: o.TokenCacheDir,
|
||||
}
|
||||
if err := cmd.GetToken.Do(ctx, in); err != nil {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
@@ -250,7 +206,6 @@ func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.SilenceUsage = true
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
206
pkg/adaptors/cmd/cmd_test.go
Normal file
206
pkg/adaptors/cmd/cmd_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"github.com/int128/kubelogin/pkg/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("get-token/Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
getToken := mock_usecases.NewMockGetToken(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, usecases.GetTokenIn{
|
||||
ListenPort: defaultListenPort,
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
getToken := mock_usecases.NewMockGetToken(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, usecases.GetTokenIn{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
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{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--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("get-token/MissingMandatoryOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
GetToken: mock_usecases.NewMockGetToken(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get-token/TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
GetToken: mock_usecases.NewMockGetToken(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
20
adaptors/env/env.go → pkg/adaptors/env/env.go
vendored
20
adaptors/env/env.go → pkg/adaptors/env/env.go
vendored
@@ -1,14 +1,12 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
@@ -36,19 +34,3 @@ func (*Env) ReadPassword(prompt string) (string, error) {
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Exec executes the command and returns the exit code.
|
||||
// Unlike the exec package, this does not return an error even if the command exited with non-zero code.
|
||||
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, xerrors.Errorf("could not execute the command: %w", err)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,Env,Logger
|
||||
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/pkg/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,Env,Logger
|
||||
|
||||
type Cmd interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
@@ -20,8 +20,8 @@ type Kubeconfig interface {
|
||||
}
|
||||
|
||||
type TokenCacheRepository interface {
|
||||
Read(filename string) (*credentialplugin.TokenCache, error)
|
||||
Write(filename string, tc credentialplugin.TokenCache) error
|
||||
FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error)
|
||||
Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error
|
||||
}
|
||||
|
||||
type CredentialPluginInteraction interface {
|
||||
@@ -42,7 +42,6 @@ type OIDCClientConfig struct {
|
||||
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) (*OIDCVerifyOut, error)
|
||||
Refresh(ctx context.Context, in OIDCRefreshIn) (*OIDCAuthenticateOut, error)
|
||||
}
|
||||
|
||||
@@ -68,26 +67,22 @@ type OIDCAuthenticateOut struct {
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
// OIDCVerifyIn represents an input DTO of OIDCClient.Verify.
|
||||
type OIDCVerifyIn struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
// OIDCVerifyIn represents an output DTO of OIDCClient.Verify.
|
||||
type OIDCVerifyOut struct {
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
// OIDCRefreshIn represents an input DTO of OIDCClient.Refresh.
|
||||
type OIDCRefreshIn struct {
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
type OIDCDecoder interface {
|
||||
DecodeIDToken(t string) (*DecodedIDToken, error)
|
||||
}
|
||||
|
||||
type DecodedIDToken struct {
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
type Env interface {
|
||||
ReadPassword(prompt string) (string, error)
|
||||
Exec(ctx context.Context, executable string, args []string) (int, error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
@@ -2,7 +2,7 @@ package kubeconfig
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
@@ -3,7 +3,7 @@ package kubeconfig
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ package kubeconfig
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
)
|
||||
|
||||
func TestKubeconfig_UpdateAuth(t *testing.T) {
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Logger.
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
)
|
||||
|
||||
type mockDebugLogger struct {
|
||||
@@ -2,7 +2,7 @@ package mock_adaptors
|
||||
|
||||
import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
)
|
||||
|
||||
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,Env,Logger)
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,Env,Logger)
|
||||
|
||||
// Package mock_adaptors is a generated GoMock package.
|
||||
package mock_adaptors
|
||||
@@ -7,9 +7,9 @@ package mock_adaptors
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
adaptors "github.com/int128/kubelogin/adaptors"
|
||||
credentialplugin "github.com/int128/kubelogin/models/credentialplugin"
|
||||
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
|
||||
adaptors "github.com/int128/kubelogin/pkg/adaptors"
|
||||
credentialplugin "github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
kubeconfig "github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
@@ -84,29 +84,29 @@ func (m *MockTokenCacheRepository) EXPECT() *MockTokenCacheRepositoryMockRecorde
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Read mocks base method
|
||||
func (m *MockTokenCacheRepository) Read(arg0 string) (*credentialplugin.TokenCache, error) {
|
||||
ret := m.ctrl.Call(m, "Read", arg0)
|
||||
// FindByKey mocks base method
|
||||
func (m *MockTokenCacheRepository) FindByKey(arg0 string, arg1 credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error) {
|
||||
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
|
||||
ret0, _ := ret[0].(*credentialplugin.TokenCache)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Read indicates an expected call of Read
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Read(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTokenCacheRepository)(nil).Read), arg0)
|
||||
// FindByKey indicates an expected call of FindByKey
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockTokenCacheRepository)(nil).FindByKey), arg0, arg1)
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
func (m *MockTokenCacheRepository) Write(arg0 string, arg1 credentialplugin.TokenCache) error {
|
||||
ret := m.ctrl.Call(m, "Write", arg0, arg1)
|
||||
// Save mocks base method
|
||||
func (m *MockTokenCacheRepository) Save(arg0 string, arg1 credentialplugin.TokenCacheKey, arg2 credentialplugin.TokenCache) error {
|
||||
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Write indicates an expected call of Write
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Write(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockTokenCacheRepository)(nil).Write), arg0, arg1)
|
||||
// Save indicates an expected call of Save
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockTokenCacheRepository)(nil).Save), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// MockCredentialPluginInteraction is a mock of CredentialPluginInteraction interface
|
||||
@@ -242,17 +242,40 @@ func (mr *MockOIDCClientMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Ca
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockOIDCClient)(nil).Refresh), arg0, arg1)
|
||||
}
|
||||
|
||||
// Verify mocks base method
|
||||
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
|
||||
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
|
||||
ret0, _ := ret[0].(*adaptors.OIDCVerifyOut)
|
||||
// MockOIDCDecoder is a mock of OIDCDecoder interface
|
||||
type MockOIDCDecoder struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockOIDCDecoderMockRecorder
|
||||
}
|
||||
|
||||
// MockOIDCDecoderMockRecorder is the mock recorder for MockOIDCDecoder
|
||||
type MockOIDCDecoderMockRecorder struct {
|
||||
mock *MockOIDCDecoder
|
||||
}
|
||||
|
||||
// NewMockOIDCDecoder creates a new mock instance
|
||||
func NewMockOIDCDecoder(ctrl *gomock.Controller) *MockOIDCDecoder {
|
||||
mock := &MockOIDCDecoder{ctrl: ctrl}
|
||||
mock.recorder = &MockOIDCDecoderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockOIDCDecoder) EXPECT() *MockOIDCDecoderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DecodeIDToken mocks base method
|
||||
func (m *MockOIDCDecoder) DecodeIDToken(arg0 string) (*adaptors.DecodedIDToken, error) {
|
||||
ret := m.ctrl.Call(m, "DecodeIDToken", arg0)
|
||||
ret0, _ := ret[0].(*adaptors.DecodedIDToken)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// 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)
|
||||
// DecodeIDToken indicates an expected call of DecodeIDToken
|
||||
func (mr *MockOIDCDecoderMockRecorder) DecodeIDToken(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeIDToken", reflect.TypeOf((*MockOIDCDecoder)(nil).DecodeIDToken), arg0)
|
||||
}
|
||||
|
||||
// MockEnv is a mock of Env interface
|
||||
@@ -278,19 +301,6 @@ 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)
|
||||
53
pkg/adaptors/oidc/decoder.go
Normal file
53
pkg/adaptors/oidc/decoder.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
type Decoder struct{}
|
||||
|
||||
// DecodeIDToken returns the claims of the ID token.
|
||||
// Note that this method does not verify the signature and always trust it.
|
||||
func (d *Decoder) DecodeIDToken(t string) (*adaptors.DecodedIDToken, error) {
|
||||
parts := strings.Split(t, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, xerrors.Errorf("token contains an invalid number of segments")
|
||||
}
|
||||
b, err := jwt.DecodeSegment(parts[1])
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the token: %w", err)
|
||||
}
|
||||
var claims jwt.StandardClaims
|
||||
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&claims); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
|
||||
}
|
||||
var rawClaims map[string]interface{}
|
||||
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&rawClaims); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
|
||||
}
|
||||
return &adaptors.DecodedIDToken{
|
||||
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
|
||||
IDTokenClaims: dumpRawClaims(rawClaims),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
|
||||
claims := make(map[string]string)
|
||||
for k, v := range rawClaims {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
claims[k] = fmt.Sprintf("%.f", v.(float64))
|
||||
default:
|
||||
claims[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
return claims
|
||||
}
|
||||
87
pkg/adaptors/oidc/decoder_test.go
Normal file
87
pkg/adaptors/oidc/decoder_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
func TestDecoder_DecodeIDToken(t *testing.T) {
|
||||
var decoder Decoder
|
||||
|
||||
t.Run("ValidToken", func(t *testing.T) {
|
||||
expiry := time.Now().Round(time.Second)
|
||||
idToken := newIDToken(t, "https://issuer.example.com", expiry)
|
||||
decodedToken, err := decoder.DecodeIDToken(idToken)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeIDToken error: %s", err)
|
||||
}
|
||||
if decodedToken.IDTokenExpiry != expiry {
|
||||
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry)
|
||||
}
|
||||
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims)
|
||||
})
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
|
||||
if err == nil {
|
||||
t.Errorf("error wants non-nil but nil")
|
||||
} else {
|
||||
t.Logf("expected error: %+v", err)
|
||||
}
|
||||
if decodedToken != nil {
|
||||
t.Errorf("decodedToken wants nil but %+v", decodedToken)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
claims := struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
}{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: expiry.Unix(),
|
||||
},
|
||||
Nonce: "NONCE",
|
||||
Groups: []string{"admin", "users"},
|
||||
EmailVerified: false,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(readPrivateKey(t, "testdata/jws.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func readPrivateKey(t *testing.T, name string) *rsa.PrivateKey {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the file: %s", err)
|
||||
}
|
||||
block, rest := pem.Decode(b)
|
||||
if block == nil {
|
||||
t.Fatalf("could not decode PEM")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
t.Fatalf("PEM should contain single key but multiple keys")
|
||||
}
|
||||
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse the key: %s", err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,7 +24,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the request: %s", err)
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
|
||||
@@ -34,7 +34,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the response: %s", err)
|
||||
return resp, err
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
@@ -6,22 +6,33 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/logging"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/tls"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidc/logging"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidc/tls"
|
||||
"github.com/int128/oauth2cli"
|
||||
"github.com/pkg/browser"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// In credential plugin mode, some browser launcher writes a message to stdout
|
||||
// and it may break the credential json for client-go.
|
||||
// This prevents the browser launcher from breaking the credential json.
|
||||
browser.Stdout = os.Stderr
|
||||
}
|
||||
|
||||
// Set provides an implementation and interface for OIDC.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Factory), "*"),
|
||||
wire.Bind(new(adaptors.OIDC), new(*Factory)),
|
||||
wire.Struct(new(Decoder)),
|
||||
wire.Bind(new(adaptors.OIDCDecoder), new(*Decoder)),
|
||||
)
|
||||
|
||||
type Factory struct {
|
||||
@@ -71,11 +82,16 @@ type client struct {
|
||||
logger adaptors.Logger
|
||||
}
|
||||
|
||||
// AuthenticateByCode performs the authorization code flow.
|
||||
func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
func (c *client) wrapContext(ctx context.Context) context.Context {
|
||||
if c.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// AuthenticateByCode performs the authorization code flow.
|
||||
func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
nonce, err := newNonce()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a nonce parameter")
|
||||
@@ -125,9 +141,7 @@ func newNonce() (string, error) {
|
||||
|
||||
// AuthenticateByPassword performs the resource owner password credentials flow.
|
||||
func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
if c.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
|
||||
}
|
||||
ctx = c.wrapContext(ctx)
|
||||
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, in.Username, in.Password)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not get a token: %w", err)
|
||||
@@ -153,32 +167,9 @@ func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAut
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify checks client ID and signature of the ID token.
|
||||
// This does not check the expiration and caller should check it.
|
||||
func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
|
||||
if c.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
|
||||
}
|
||||
verifier := c.provider.Verifier(&oidc.Config{
|
||||
ClientID: c.oauth2Config.ClientID,
|
||||
SkipExpiryCheck: true,
|
||||
})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
|
||||
}
|
||||
claims, err := dumpClaims(verifiedIDToken)
|
||||
if err != nil {
|
||||
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
|
||||
}
|
||||
return &adaptors.OIDCVerifyOut{
|
||||
IDTokenExpiry: verifiedIDToken.Expiry,
|
||||
IDTokenClaims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refresh sends a refresh token request and returns a token set.
|
||||
func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
currentToken := &oauth2.Token{
|
||||
Expiry: time.Now(),
|
||||
RefreshToken: in.RefreshToken,
|
||||
@@ -212,17 +203,5 @@ func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adapt
|
||||
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
|
||||
var rawClaims map[string]interface{}
|
||||
err := token.Claims(&rawClaims)
|
||||
claims := make(map[string]string)
|
||||
for k, v := range rawClaims {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
claims[k] = fmt.Sprintf("%f", v.(float64))
|
||||
default:
|
||||
claims[k] = fmt.Sprintf("%s", v)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return claims, xerrors.Errorf("error while decoding the ID token: %w", err)
|
||||
}
|
||||
return claims, nil
|
||||
return dumpRawClaims(rawClaims), err
|
||||
}
|
||||
8
pkg/adaptors/oidc/testdata/Makefile
vendored
Normal file
8
pkg/adaptors/oidc/testdata/Makefile
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
all: jws.key
|
||||
|
||||
jws.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -v jws.key
|
||||
15
pkg/adaptors/oidc/testdata/jws.key
vendored
Normal file
15
pkg/adaptors/oidc/testdata/jws.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQCrH34yA/f/sBOUlkYnRtd2jgDZ3WivhidqvoQaa73xqTazbkn6
|
||||
GZ9r7jx0CGLRV2bmErj2WoyT54yrhezrKh0YXAHlrwLdsmV4dwiV0lOfUJd9P/vF
|
||||
e2hiAWv4CcO9ZuNkTsrxM5W8Wdj2tjqOvsIn4We+HWPkpknT7VtT5RrumwIDAQAB
|
||||
AoGAFqy5oA7+kZbXQV0YNqQgcMkoO7Ym5Ps1xeMwxf94z8jIQsZebxFuGnMa95UU
|
||||
4wBd1ias85fUANUxwpigaBjQee5Hk+dnfUe1snUWYNm9H6tKrXEF8ajer3a2knEv
|
||||
GfK0CSEumFougfW2xG88ChGTS60wc+MIRfXERCvWpGm/5EECQQDdv5IBSi89g/R1
|
||||
5AGZKFCoqr6Zw5bWEKPzCCYJZzncR1ER9vP2AnMExM8Io/87WYvmpZIUrXJvQYm8
|
||||
hkfVOcBZAkEAxY4VcqmRWru3zmnbj21MwcwtgESaONkWsHeYs1C/Y/3zt7TuelYz
|
||||
ZJ9aUuUsaiJLEs9Y26nMt0L0snWGr2noEwJBANaDp1PWFyMUTt3pB17JcFXqb15C
|
||||
pt1I1cGapWk9Uez1lMijNNhNAEWhuoKqW5Nnif5DN7EHJYfZR8x3vm/YYWkCQHAA
|
||||
0iAkCwjKDLe2RIjYiwAE5ncmbdl1GuwJokVnrlrei+LHbb1mSdTuk6MT006JCs8r
|
||||
R1GivzHXgCv9fdLN1IkCQHxRvv9RPND80eEkdMv4qu0s22OLRhLQ/pb+YeT5Cjjv
|
||||
pJYWKrvXdRZcuNde9JiiTgK2UW1wM8KeD/EGvK2yF6M=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,9 +1,8 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: ca1.crt ca1.crt.base64 ca2.crt ca2.crt.base64 ca3.crt ca3.crt.base64
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -v *.key *.csr *.crt *.base64
|
||||
-rm -v *.key *.csr *.crt *.base64
|
||||
|
||||
%.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
)
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
64
pkg/adaptors/tokencache/tokencache.go
Normal file
64
pkg/adaptors/tokencache/tokencache.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package tokencache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Repository), "*"),
|
||||
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
|
||||
)
|
||||
|
||||
// Repository provides access to the token cache on the local filesystem.
|
||||
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
|
||||
type Repository struct{}
|
||||
|
||||
func (r *Repository) FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error) {
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
var c credentialplugin.TokenCache
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return xerrors.Errorf("could not create directory %s: %w", dir, err)
|
||||
}
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not create file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
e := json.NewEncoder(f)
|
||||
if err := e.Encode(&cache); err != nil {
|
||||
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func computeFilename(key credentialplugin.TokenCacheKey) string {
|
||||
s := sha256.New()
|
||||
_, _ = s.Write([]byte(key.IssuerURL))
|
||||
_, _ = s.Write([]byte{0x00})
|
||||
_, _ = s.Write([]byte(key.ClientID))
|
||||
return hex.EncodeToString(s.Sum(nil))
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
)
|
||||
|
||||
func TestRepository_Read(t *testing.T) {
|
||||
func TestRepository_FindByKey(t *testing.T) {
|
||||
var r Repository
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
@@ -23,13 +23,17 @@ func TestRepository_Read(t *testing.T) {
|
||||
t.Errorf("could not clean up the temp dir: %s", err)
|
||||
}
|
||||
}()
|
||||
key := credentialplugin.TokenCacheKey{
|
||||
IssuerURL: "YOUR_ISSUER",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}
|
||||
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
|
||||
filename := filepath.Join(dir, "token-cache")
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
if err := ioutil.WriteFile(filename, []byte(json), 0600); err != nil {
|
||||
t.Fatalf("could not write to the temp file: %s", err)
|
||||
}
|
||||
|
||||
tokenCache, err := r.Read(filename)
|
||||
tokenCache, err := r.FindByKey(dir, key)
|
||||
if err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
@@ -40,7 +44,7 @@ func TestRepository_Read(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_Write(t *testing.T) {
|
||||
func TestRepository_Save(t *testing.T) {
|
||||
var r Repository
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
@@ -54,12 +58,16 @@ func TestRepository_Write(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
filename := filepath.Join(dir, "token-cache")
|
||||
key := credentialplugin.TokenCacheKey{
|
||||
IssuerURL: "YOUR_ISSUER",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}
|
||||
tokenCache := credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
|
||||
if err := r.Write(filename, tokenCache); err != nil {
|
||||
if err := r.Save(dir, key, tokenCache); err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the token cache file: %s", err)
|
||||
@@ -5,18 +5,18 @@ package di
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
credentialPluginAdaptor "github.com/int128/kubelogin/adaptors/credentialplugin"
|
||||
"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/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/cmd"
|
||||
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/login"
|
||||
)
|
||||
|
||||
// NewCmd returns an instance of adaptors.Cmd.
|
||||
@@ -6,18 +6,18 @@
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/adaptors/credentialplugin"
|
||||
"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/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
credentialplugin2 "github.com/int128/kubelogin/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/login"
|
||||
)
|
||||
|
||||
// Injectors from di.go:
|
||||
@@ -27,12 +27,14 @@ func NewCmd() adaptors.Cmd {
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
decoder := &oidc.Decoder{}
|
||||
envEnv := &env.Env{}
|
||||
showLocalServerURL := &auth.ShowLocalServerURL{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
authentication := &auth.Authentication{
|
||||
OIDC: factory,
|
||||
OIDCDecoder: decoder,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
ShowLocalServerURL: showLocalServerURL,
|
||||
@@ -51,17 +53,10 @@ func NewCmd() adaptors.Cmd {
|
||||
Interaction: interaction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
@@ -70,9 +65,11 @@ func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL u
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
decoder := &oidc.Decoder{}
|
||||
envEnv := &env.Env{}
|
||||
authentication := &auth.Authentication{
|
||||
OIDC: factory,
|
||||
OIDCDecoder: decoder,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
ShowLocalServerURL: loginShowLocalServerURL,
|
||||
@@ -90,17 +87,10 @@ func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL u
|
||||
Interaction: credentialPluginInteraction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
@@ -3,7 +3,13 @@ package credentialplugin
|
||||
|
||||
import "time"
|
||||
|
||||
// TokenCache represents a token object cached.
|
||||
// TokenCacheKey represents a key of a token cache.
|
||||
type TokenCacheKey struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
}
|
||||
|
||||
// TokenCache represents a token cache.
|
||||
type TokenCache struct {
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -39,12 +39,36 @@ const passwordPrompt = "Password: "
|
||||
//
|
||||
type Authentication struct {
|
||||
OIDC adaptors.OIDC
|
||||
OIDCDecoder adaptors.OIDCDecoder
|
||||
Env adaptors.Env
|
||||
Logger adaptors.Logger
|
||||
ShowLocalServerURL usecases.LoginShowLocalServerURL
|
||||
}
|
||||
|
||||
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
|
||||
if in.OIDCConfig.IDToken != "" {
|
||||
u.Logger.Debugf(1, "checking expiration of the existing token")
|
||||
// Skip verification of the token to reduce time of a discovery request.
|
||||
// Here it trusts the signature and claims and checks only expiration,
|
||||
// because the token has been verified before caching.
|
||||
token, err := u.OIDCDecoder.DecodeIDToken(in.OIDCConfig.IDToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
|
||||
}
|
||||
if token.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
|
||||
u.Logger.Debugf(1, "you already have a valid token until %s", token.IDTokenExpiry)
|
||||
return &usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: in.OIDCConfig.IDToken,
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
IDTokenExpiry: token.IDTokenExpiry,
|
||||
IDTokenClaims: token.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.Debugf(1, "you have an expired token at %s", token.IDTokenExpiry)
|
||||
}
|
||||
|
||||
u.Logger.Debugf(1, "initializing an OIDC client")
|
||||
client, err := u.OIDC.New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.OIDCConfig,
|
||||
CACertFilename: in.CACertFilename,
|
||||
@@ -54,27 +78,8 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
|
||||
return nil, xerrors.Errorf("could not create an OIDC client: %w", err)
|
||||
}
|
||||
|
||||
if in.OIDCConfig.IDToken != "" {
|
||||
u.Logger.Debugf(1, "Verifying the existing token")
|
||||
out, err := client.Verify(ctx, adaptors.OIDCVerifyIn{IDToken: in.OIDCConfig.IDToken})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("you need to remove the existing token manually: %w", err)
|
||||
}
|
||||
if out.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
|
||||
u.Logger.Debugf(1, "You already have a valid token")
|
||||
return &usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: in.OIDCConfig.IDToken,
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.Debugf(1, "You have an expired token at %s", out.IDTokenExpiry)
|
||||
}
|
||||
|
||||
if in.OIDCConfig.RefreshToken != "" {
|
||||
u.Logger.Debugf(1, "Refreshing the token")
|
||||
u.Logger.Debugf(1, "refreshing the token")
|
||||
out, err := client.Refresh(ctx, adaptors.OIDCRefreshIn{
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
})
|
||||
@@ -86,11 +91,11 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.Debugf(1, "Could not refresh the token: %s", err)
|
||||
u.Logger.Debugf(1, "could not refresh the token: %s", err)
|
||||
}
|
||||
|
||||
if in.Username == "" {
|
||||
u.Logger.Debugf(1, "Performing the authentication code flow")
|
||||
u.Logger.Debugf(1, "performing the authentication code flow")
|
||||
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
|
||||
LocalServerPort: in.ListenPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
@@ -107,7 +112,7 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
u.Logger.Debugf(1, "Performing the resource owner password credentials flow")
|
||||
u.Logger.Debugf(1, "performing the resource owner password credentials flow")
|
||||
if in.Password == "" {
|
||||
in.Password, err = u.Env.ReadPassword(passwordPrompt)
|
||||
if err != nil {
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"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/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -220,22 +220,17 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "VALID_ID_TOKEN"}).
|
||||
Return(&adaptors.OIDCVerifyOut{
|
||||
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("VALID_ID_TOKEN").
|
||||
Return(&adaptors.DecodedIDToken{
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
OIDC: mock_adaptors.NewMockOIDC(ctrl),
|
||||
OIDCDecoder: mockOIDCDecoder,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
@@ -264,13 +259,14 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
|
||||
Return(&adaptors.OIDCVerifyOut{
|
||||
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("EXPIRED_ID_TOKEN").
|
||||
Return(&adaptors.DecodedIDToken{
|
||||
IDTokenExpiry: pastTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Refresh(ctx, adaptors.OIDCRefreshIn{
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
@@ -288,8 +284,9 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
OIDC: mockOIDC,
|
||||
OIDCDecoder: mockOIDCDecoder,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
@@ -319,13 +316,14 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
|
||||
Return(&adaptors.OIDCVerifyOut{
|
||||
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("EXPIRED_ID_TOKEN").
|
||||
Return(&adaptors.DecodedIDToken{
|
||||
IDTokenExpiry: pastTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Refresh(ctx, adaptors.OIDCRefreshIn{
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
@@ -348,8 +346,9 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
OIDC: mockOIDC,
|
||||
OIDCDecoder: mockOIDCDecoder,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -29,10 +29,12 @@ type GetToken struct {
|
||||
func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
|
||||
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
|
||||
|
||||
tokenCache, err := u.TokenCacheRepository.Read(in.TokenCacheFilename)
|
||||
u.Logger.Debugf(1, "finding a token from cache directory %s", in.TokenCacheDir)
|
||||
cacheKey := credentialplugin.TokenCacheKey{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
|
||||
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
|
||||
if err != nil {
|
||||
u.Logger.Debugf(1, "could not read the token cache file: %s", err)
|
||||
tokenCache = &credentialplugin.TokenCache{}
|
||||
u.Logger.Debugf(1, "could not find a token cache: %s", err)
|
||||
cache = &credentialplugin.TokenCache{}
|
||||
}
|
||||
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
@@ -40,8 +42,8 @@ func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
IDToken: tokenCache.IDToken,
|
||||
RefreshToken: tokenCache.RefreshToken,
|
||||
IDToken: cache.IDToken,
|
||||
RefreshToken: cache.RefreshToken,
|
||||
},
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
ListenPort: in.ListenPort,
|
||||
@@ -54,20 +56,22 @@ func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
for k, v := range out.IDTokenClaims {
|
||||
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
|
||||
u.Logger.Debugf(1, "the ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
if !out.AlreadyHasValidIDToken {
|
||||
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
|
||||
if err := u.TokenCacheRepository.Write(in.TokenCacheFilename, credentialplugin.TokenCache{
|
||||
cache := credentialplugin.TokenCache{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
}); err != nil {
|
||||
}
|
||||
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, cacheKey, cache); err != nil {
|
||||
return xerrors.Errorf("could not write the token cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
u.Logger.Debugf(1, "writing the token to client-go")
|
||||
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenExpiry}); err != nil {
|
||||
return xerrors.Errorf("could not write a credential object: %w", err)
|
||||
return xerrors.Errorf("could not write the token to client-go: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -23,16 +23,16 @@ func TestGetToken_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
@@ -57,13 +57,21 @@ func TestGetToken_Do(t *testing.T) {
|
||||
}, nil)
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}).
|
||||
Return(nil, xerrors.New("file not found"))
|
||||
tokenCacheRepository.EXPECT().
|
||||
Write("/path/to/token-cache", credentialplugin.TokenCache{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
Save("/path/to/token-cache",
|
||||
credentialplugin.TokenCacheKey{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
},
|
||||
credentialplugin.TokenCache{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(credentialplugin.Output{
|
||||
@@ -86,10 +94,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
@@ -109,7 +117,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
}, nil)
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}).
|
||||
Return(&credentialplugin.TokenCache{
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
}, nil)
|
||||
@@ -135,10 +146,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
@@ -152,7 +163,10 @@ func TestGetToken_Do(t *testing.T) {
|
||||
Return(nil, xerrors.New("authentication error"))
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}).
|
||||
Return(nil, xerrors.New("file not found"))
|
||||
u := GetToken{
|
||||
Authentication: mockAuthentication,
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases Login,LoginAndExec,GetToken,Authentication
|
||||
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/pkg/usecases Login,GetToken,Authentication
|
||||
|
||||
type Login interface {
|
||||
Do(ctx context.Context, in LoginIn) error
|
||||
@@ -38,32 +38,17 @@ type GetToken interface {
|
||||
|
||||
// GetTokenIn represents an input DTO of the GetToken use-case.
|
||||
type GetTokenIn struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
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
|
||||
TokenCacheFilename string
|
||||
}
|
||||
|
||||
type LoginAndExec interface {
|
||||
Do(ctx context.Context, in LoginAndExecIn) (*LoginAndExecOut, error)
|
||||
}
|
||||
|
||||
// LoginAndExecInIn represents an input DTO of the LoginAndExec use-case.
|
||||
type LoginAndExecIn struct {
|
||||
LoginIn LoginIn
|
||||
Executable string
|
||||
Args []string
|
||||
}
|
||||
|
||||
type LoginAndExecOut struct {
|
||||
ExitCode int
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string // optional
|
||||
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
|
||||
TokenCacheDir string
|
||||
}
|
||||
|
||||
type Authentication interface {
|
||||
@@ -4,17 +4,15 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides the use-cases of logging in.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Login), "*"),
|
||||
wire.Struct(new(Exec), "*"),
|
||||
wire.Bind(new(usecases.Login), new(*Login)),
|
||||
wire.Bind(new(usecases.LoginAndExec), new(*Exec)),
|
||||
)
|
||||
|
||||
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
|
||||
@@ -44,8 +42,8 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
u.Logger.Printf(oidcConfigErrorMessage)
|
||||
return xerrors.Errorf("could not find the current authentication provider: %w", err)
|
||||
}
|
||||
u.Logger.Debugf(1, "Using the authentication provider of the user %s", authProvider.UserName)
|
||||
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
|
||||
u.Logger.Debugf(1, "using the authentication provider of the user %s", authProvider.UserName)
|
||||
u.Logger.Debugf(1, "a token will be written to %s", authProvider.LocationOfOrigin)
|
||||
|
||||
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: authProvider.OIDCConfig,
|
||||
@@ -60,7 +58,7 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
for k, v := range out.IDTokenClaims {
|
||||
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
|
||||
u.Logger.Debugf(1, "the ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
if out.AlreadyHasValidIDToken {
|
||||
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
|
||||
@@ -70,7 +68,7 @@ func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
|
||||
authProvider.OIDCConfig.IDToken = out.IDToken
|
||||
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
|
||||
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
|
||||
u.Logger.Debugf(1, "writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
|
||||
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
|
||||
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/pkg/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/usecases"
|
||||
"github.com/int128/kubelogin/pkg/usecases/mock_usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/usecases (interfaces: Login,LoginAndExec,GetToken,Authentication)
|
||||
// Source: github.com/int128/kubelogin/pkg/usecases (interfaces: Login,GetToken,Authentication)
|
||||
|
||||
// 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"
|
||||
usecases "github.com/int128/kubelogin/usecases"
|
||||
usecases "github.com/int128/kubelogin/pkg/usecases"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
@@ -46,42 +46,6 @@ 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)
|
||||
}
|
||||
|
||||
// MockGetToken is a mock of GetToken interface
|
||||
type MockGetToken struct {
|
||||
ctrl *gomock.Controller
|
||||
@@ -1,77 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Exec provide the use case of transparently executing kubectl.
|
||||
//
|
||||
// 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 {
|
||||
Authentication usecases.Authentication
|
||||
Kubeconfig adaptors.Kubeconfig
|
||||
Env adaptors.Env
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (u *Exec) Do(ctx context.Context, in usecases.LoginAndExecIn) (*usecases.LoginAndExecOut, error) {
|
||||
if err := u.login(ctx, in.LoginIn); err != nil {
|
||||
return nil, xerrors.Errorf("could not log in to the provider: %w", err)
|
||||
}
|
||||
u.Logger.Debugf(1, "Executing the command %s %s", in.Executable, in.Args)
|
||||
exitCode, err := u.Env.Exec(ctx, in.Executable, in.Args)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not execute kubectl: %w", err)
|
||||
}
|
||||
u.Logger.Debugf(1, "The command exited with status %d", exitCode)
|
||||
return &usecases.LoginAndExecOut{ExitCode: exitCode}, nil
|
||||
}
|
||||
|
||||
func (u *Exec) login(ctx context.Context, in usecases.LoginIn) error {
|
||||
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
|
||||
|
||||
authProvider, err := u.Kubeconfig.GetCurrentAuthProvider(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", authProvider.UserName)
|
||||
u.Logger.Debugf(1, "A token will be written to %s", authProvider.LocationOfOrigin)
|
||||
|
||||
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: authProvider.OIDCConfig,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
ListenPort: in.ListenPort,
|
||||
Username: in.Username,
|
||||
Password: in.Password,
|
||||
CACertFilename: in.CACertFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
for k, v := range out.IDTokenClaims {
|
||||
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
if out.AlreadyHasValidIDToken {
|
||||
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
|
||||
authProvider.OIDCConfig.IDToken = out.IDToken
|
||||
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
|
||||
|
||||
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
|
||||
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
|
||||
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func TestExec_Do(t *testing.T) {
|
||||
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
|
||||
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
|
||||
|
||||
t.Run("FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.LoginAndExecIn{
|
||||
Executable: "kubectl",
|
||||
Args: []string{"foo", "bar"},
|
||||
LoginIn: usecases.LoginIn{
|
||||
KubeconfigFilename: "/path/to/kubeconfig",
|
||||
KubeconfigContext: "theContext",
|
||||
KubeconfigUser: "theUser",
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
},
|
||||
}
|
||||
currentAuthProvider := &kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "google",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
|
||||
mockKubeconfig.EXPECT().
|
||||
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
|
||||
Return(currentAuthProvider, nil)
|
||||
mockKubeconfig.EXPECT().
|
||||
UpdateAuthProvider(&kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "google",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
},
|
||||
})
|
||||
mockEnv := mock_adaptors.NewMockEnv(ctrl)
|
||||
mockEnv.EXPECT().
|
||||
Exec(ctx, "kubectl", []string{"foo", "bar"}).
|
||||
Return(123, nil)
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: currentAuthProvider.OIDCConfig,
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}).
|
||||
Return(&usecases.AuthenticationOut{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
u := Exec{
|
||||
Authentication: mockAuthentication,
|
||||
Kubeconfig: mockKubeconfig,
|
||||
Env: mockEnv,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &usecases.LoginAndExecOut{
|
||||
ExitCode: 123,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HasValidIDToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.LoginAndExecIn{
|
||||
Executable: "kubectl",
|
||||
Args: []string{"foo", "bar"},
|
||||
}
|
||||
currentAuthProvider := &kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "theUser",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
},
|
||||
}
|
||||
mockEnv := mock_adaptors.NewMockEnv(ctrl)
|
||||
mockEnv.EXPECT().
|
||||
Exec(ctx, "kubectl", []string{"foo", "bar"}).
|
||||
Return(0, nil)
|
||||
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
|
||||
mockKubeconfig.EXPECT().
|
||||
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
|
||||
Return(currentAuthProvider, nil)
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
|
||||
Return(&usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: "VALID_ID_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
u := Exec{
|
||||
Authentication: mockAuthentication,
|
||||
Kubeconfig: mockKubeconfig,
|
||||
Env: mockEnv,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &usecases.LoginAndExecOut{
|
||||
ExitCode: 0,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoOIDCConfig", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.LoginAndExecIn{
|
||||
Executable: "kubectl",
|
||||
Args: []string{"foo", "bar"},
|
||||
LoginIn: usecases.LoginIn{},
|
||||
}
|
||||
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
|
||||
mockKubeconfig.EXPECT().
|
||||
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
|
||||
Return(nil, xerrors.New("no oidc config"))
|
||||
mockEnv := mock_adaptors.NewMockEnv(ctrl)
|
||||
mockEnv.EXPECT().
|
||||
Exec(ctx, "kubectl", []string{"foo", "bar"}).
|
||||
Return(0, nil)
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
u := Exec{
|
||||
Authentication: mockAuthentication,
|
||||
Kubeconfig: mockKubeconfig,
|
||||
Env: mockEnv,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &usecases.LoginAndExecOut{
|
||||
ExitCode: 0,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AuthenticationError", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.LoginAndExecIn{}
|
||||
currentAuthProvider := &kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "google",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
|
||||
mockKubeconfig.EXPECT().
|
||||
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
|
||||
Return(currentAuthProvider, nil)
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
|
||||
Return(nil, xerrors.New("authentication error"))
|
||||
u := Exec{
|
||||
Authentication: mockAuthentication,
|
||||
Kubeconfig: mockKubeconfig,
|
||||
Env: mock_adaptors.NewMockEnv(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err == nil {
|
||||
t.Errorf("err wants non-nil but nil")
|
||||
}
|
||||
if out != nil {
|
||||
t.Errorf("out wants nil but %+v", out)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WriteError", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.LoginAndExecIn{}
|
||||
currentAuthProvider := &kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "google",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
},
|
||||
}
|
||||
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
|
||||
mockKubeconfig.EXPECT().
|
||||
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
|
||||
Return(currentAuthProvider, nil)
|
||||
mockKubeconfig.EXPECT().
|
||||
UpdateAuthProvider(&kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "google",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
},
|
||||
}).
|
||||
Return(xerrors.New("I/O error"))
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication.EXPECT().
|
||||
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
|
||||
Return(&usecases.AuthenticationOut{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
u := Exec{
|
||||
Authentication: mockAuthentication,
|
||||
Kubeconfig: mockKubeconfig,
|
||||
Env: mock_adaptors.NewMockEnv(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err == nil {
|
||||
t.Errorf("err wants non-nil but nil")
|
||||
}
|
||||
if out != nil {
|
||||
t.Errorf("out wants nil but %+v", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user