Compare commits

..

13 Commits

Author SHA1 Message Date
Hidetake Iwata
25c7c1e703 Add snapcraft.yaml (#147)
* Create snapcraft.yaml

* Update snapcraft.yaml
2019-09-11 10:01:25 +09:00
Hidetake Iwata
6b1e11f071 Refactor: use channel to wait for opening browser (#143) 2019-08-30 20:41:36 +09:00
Hidetake Iwata
554daf7655 Fix doc: add klog options 2019-08-29 18:28:59 +09:00
Hidetake Iwata
d67d16b333 Fix README.md: add klog options 2019-08-29 18:01:19 +09:00
dependabot-preview[bot]
3d0973054b Bump k8s.io/klog from 0.3.1 to 0.4.0 (#140)
Bumps [k8s.io/klog](https://github.com/kubernetes/klog) from 0.3.1 to 0.4.0.
- [Release notes](https://github.com/kubernetes/klog/releases)
- [Changelog](https://github.com/kubernetes/klog/blob/master/RELEASE.md)
- [Commits](https://github.com/kubernetes/klog/compare/v0.3.1...v0.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-29 17:54:40 +09:00
Hidetake Iwata
bf02210f2a Refactor: merge interface and implementation package (#141)
* Refactor: move logger interfaces

* Refactor: move oidc interfaces

* Refactor: move env interface

* Refactor: move credential plugin interface

* Refactor: move token cache interface

* Refactor: move kubeconfig interface

* Refactor: move cmd interface

* Refactor: move use-cases interfaces
2019-08-28 22:55:28 +09:00
Hidetake Iwata
53e8284b63 Move to k8s.io/klog (#139) 2019-08-27 14:48:44 +09:00
Hidetake Iwata
d9b8d99fae Refactor docs (#138)
* Refactor: extract standalone-mode.md

* Refactor: remove diagram

* Refactor: remove DESIGN.md

* Refactor: change README.md
2019-08-27 11:51:48 +09:00
Hidetake Iwata
3e30346c9b Update README.md 2019-08-23 16:33:53 +09:00
Hidetake Iwata
1e80481145 Refactor: split commands (#137) 2019-08-22 17:22:03 +09:00
dependabot-preview[bot]
9242b1917b Bump github.com/coreos/go-oidc (#136)
Bumps [github.com/coreos/go-oidc](https://github.com/coreos/go-oidc) from 2.0.0+incompatible to 2.1.0+incompatible.
- [Release notes](https://github.com/coreos/go-oidc/releases)
- [Commits](https://github.com/coreos/go-oidc/compare/v2.0.0...v2.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-22 09:47:42 +09:00
dependabot-preview[bot]
306bf09485 Bump github.com/go-test/deep from 1.0.2 to 1.0.3 (#135)
Bumps [github.com/go-test/deep](https://github.com/go-test/deep) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/go-test/deep/releases)
- [Changelog](https://github.com/go-test/deep/blob/master/CHANGES.md)
- [Commits](https://github.com/go-test/deep/compare/v1.0.2...v1.0.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-20 10:20:07 +09:00
Hidetake Iwata
4ad77cd5f8 Update README.md 2019-08-19 23:24:23 +09:00
70 changed files with 2113 additions and 2194 deletions

View File

@@ -1,47 +0,0 @@
# Design of kubelogin
This explains design of kubelogin.
## Use cases
Kubelogin is a command line tool and designed to run as both a standalone command and a kubectl plugin.
It respects the following flags, commonly used in kubectl:
```
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-v, --v int If set to 1 or greater, it shows debug log
```
As well as it respects the environment variable `KUBECONFIG`.
### Login by the command
TODO
## Architecture
Kubelogin consists of the following layers:
- `usecases`: This provides the use-cases.
- `adaptor`: This provides external access and converts objects between the use-cases and external system.
### Use-cases
This provides the use-cases mentioned in the previous section.
This layer should not contain external access such as HTTP requests and system calls.
### Adaptor
This provides external access such as command line interface and HTTP requests.

View File

@@ -20,11 +20,6 @@ $(TARGET_PLUGIN): $(TARGET)
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
diagram: docs/authn.png
%.png: %.seqdiag
seqdiag -a -f /Library/Fonts/Verdana.ttf $<
dist:
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
mv dist/gh/kubelogin.rb dist/

254
README.md
View File

@@ -3,47 +3,39 @@
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`.
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.
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 access Kubernetes APIs with the token.
## Getting Started
You can install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
### Setup
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
```sh
# Homebrew
brew tap int128/kubelogin
brew install kubelogin
brew install int128/kubelogin/kubelogin
# Krew
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.1/kubelogin_linux_amd64.zip
curl -LO https://github.com/int128/kubelogin/releases/download/v1.14.2/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
You need to configure the OIDC provider, Kubernetes API server, kubeconfig and role binding.
You need to configure the OIDC provider, Kubernetes API server and role binding.
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)
You can run kubelogin as the following methods:
- Credential plugin mode
- Standalone mode
### 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.
Configure the kubeconfig like:
Configure the kubeconfig to run kubelogin as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
It provides transparent login without manually running kubelogin command.
For example,
```yaml
users:
@@ -60,6 +52,8 @@ users:
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
### Run
Run kubectl.
```sh
@@ -90,90 +84,17 @@ 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.
### Standalone mode
You can run kubelogin as a standalone command.
In this method, you need to manually run the command before running kubectl.
Configure the kubeconfig like:
```yaml
- name: keycloak
user:
auth-provider:
config:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: https://issuer.example.com
name: oidc
```
Run kubelogin:
```sh
kubelogin
# or run as a kubectl plugin
kubectl oidc-login
```
It automatically opens the browser and you can log in to the provider.
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
After authentication, kubelogin writes the ID token and refresh token to the kubeconfig.
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
Updated ~/.kubeconfig
```
Now you can access the cluster.
```
% kubectl get pods
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.
```
% kubelogin
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
```
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.
As well as you can update the ID token in the kubeconfig by running the command.
See [standalone mode](docs/standalone-mode.md) for more.
## Configuration
## Usage
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).
### Credential plugin mode
Kubelogin supports the following options:
```
@@ -194,12 +115,27 @@ Flags:
--oidc-extra-scope strings Scopes to request to the provider
--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-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
-h, --help help for get-token
Global Flags:
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
#### Extra scopes
See also the options in [standalone mode](docs/standalone-mode.md).
### Extra scopes
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
@@ -208,110 +144,14 @@ You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
- --oidc-extra-scope=profile
```
#### CA Certificates
### CA Certificates
You can use your self-signed certificates for the provider.
You can use your self-signed certificate for the provider.
```yaml
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
```
### Standalone mode
Kubelogin supports the following options:
```
% kubelogin -h
Login to the OpenID Connect provider and update the kubeconfig
Usage:
kubelogin [flags]
kubelogin [command]
Examples:
# Login to the provider using the authorization code flow.
kubelogin
# Login to the provider using the resource owner password credentials flow.
kubelogin --username USERNAME --password PASSWORD
# Run as a credential plugin.
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
Available Commands:
get-token Run as a kubectl credential plugin
help Help about any command
version Print the version information
Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-v, --v int If set to 1 or greater, it shows debug log
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
-h, --help help for kubelogin
```
#### Kubeconfig
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
It defaults to `~/.kube/config`.
```sh
# by the option
kubelogin --kubeconfig /path/to/kubeconfig
# by the environment variable
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
```
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
Kubelogin supports the following keys of `auth-provider` in a kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
Key | Direction | Value
----|-----------|------
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
`client-id` | Read (Mandatory) | Client ID of the provider.
`client-secret` | Read (Mandatory) | Client Secret of the provider.
`idp-certificate-authority` | Read | CA certificate path of the provider.
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
`id-token` | Write | ID token got from the provider.
`refresh-token` | Write | Refresh token got from the provider.
#### Extra scopes
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
#### CA Certificates
You can use your self-signed certificates for the provider.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```
### HTTP Proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
@@ -332,15 +172,11 @@ You need to register the following redirect URIs to the provider:
You can change the ports by the option:
```sh
# run as a standalone command
kubelogin --listen-port 12345 --listen-port 23456
# run as a credential plugin
kubelogin get-token --listen-port 12345 --listen-port 23456
```yaml
- --listen-port 12345
- --listen-port 23456
```
#### Resource owner password credentials grant flow
As well as you can use the resource owner password credentials grant flow.
@@ -349,11 +185,12 @@ Most OIDC providers do not support this flow.
You can pass the username and password:
```
% kubelogin --username USER --password PASS
```yaml
- --username USERNAME
- --password PASSWORD
```
or use the password prompt:
If the password is not set, kubelogin will show the prompt.
```
% kubelogin --username USER
@@ -361,6 +198,13 @@ Password:
```
## Related works
### Kubernetes Dashboard
You can access the Kubernetes Dashboard using kubelogin and [kauthproxy](https://github.com/int128/kauthproxy).
## Contributions
This is an open source software licensed under Apache License 2.0.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -1,16 +0,0 @@
seqdiag {
User -> kubelogin [label = "execute"];
kubelogin -> Browser [label = "open"];
Browser -> Provider [label = "authentication request"];
Browser <-- Provider [label = "redirect"];
User -> Browser [label = "enter credentials"];
Browser -> Provider [label = "credentials"];
Browser <-- Provider [label = "authentication response"];
User <-- Browser [label = "success"];
kubelogin <-- Browser [label = "close"];
kubelogin -> Provider [label = "token request"];
kubelogin <-- Provider [label = "token response"];
kubelogin -> kubeconfig [label = "write token"];
kubelogin <-- kubeconfig;
User <-- kubelogin;
}

View File

@@ -49,9 +49,9 @@ kubectl oidc-login get-token -v1 \
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
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=kubernetes
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://dex.example.com
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=YOUR_SUBJECT
```
## 3. Setup Kubernetes API server

View File

@@ -25,9 +25,9 @@ kubectl oidc-login get-token -v1 \
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
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=YOUR_CLIENT_ID.apps.googleusercontent.com
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://accounts.google.com
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=YOUR_SUBJECT
```
## 2. Setup Kubernetes API server

View File

@@ -40,10 +40,10 @@ kubectl oidc-login get-token -v1 \
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]
I0827 12:29:03.086476 23722 get_token.go:59] the ID token has the claim: groups=[kubernetes:admin]
I0827 12:29:03.086531 23722 get_token.go:59] the ID token has the claim: aud=kubernetes
I0827 12:29:03.086553 23722 get_token.go:59] the ID token has the claim: iss=https://keycloak.example.com/auth/realms/YOUR_REALM
I0827 12:29:03.086561 23722 get_token.go:59] the ID token has the claim: sub=f08655e2-901f-48e5-8c64-bb9f7784d5df
```
## 2. Setup Kubernetes API server

180
docs/standalone-mode.md Normal file
View File

@@ -0,0 +1,180 @@
# Standalone mode
You can run kubelogin as a standalone command.
In this mode, you need to manually run the command before running kubectl.
Configure the kubeconfig like:
```yaml
- name: keycloak
user:
auth-provider:
config:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: https://issuer.example.com
name: oidc
```
Run kubelogin:
```sh
kubelogin
# or run as a kubectl plugin
kubectl oidc-login
```
It automatically opens the browser and you can log in to the provider.
<img src="keycloak-login.png" alt="keycloak-login" width="455" height="329">
After authentication, kubelogin writes the ID token and refresh token to the kubeconfig.
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
Updated ~/.kubeconfig
```
Now you can access the cluster.
```
% kubectl get pods
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.
```
% kubelogin
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
```
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.
## Usage
Kubelogin supports the following options:
```
% kubelogin -h
Login to the OpenID Connect provider and update the kubeconfig
Usage:
kubelogin [flags]
kubelogin [command]
Examples:
# Login to the provider using the authorization code flow.
kubelogin
# Login to the provider using the resource owner password credentials flow.
kubelogin --username USERNAME --password PASSWORD
# Run as a credential plugin.
kubelogin get-token --oidc-issuer-url=https://issuer.example.com
Available Commands:
get-token Run as a kubectl credential plugin
help Help about any command
version Print the version information
Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
--skip-open-browser If true, it does not open the browser on authentication
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
-h, --help help for kubelogin
--version version for kubelogin
```
### Kubeconfig
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
It defaults to `~/.kube/config`.
```sh
# by the option
kubelogin --kubeconfig /path/to/kubeconfig
# by the environment variable
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
```
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
Kubelogin supports the following keys of `auth-provider` in a kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
Key | Direction | Value
----|-----------|------
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
`client-id` | Read (Mandatory) | Client ID of the provider.
`client-secret` | Read (Mandatory) | Client Secret of the provider.
`idp-certificate-authority` | Read | CA certificate path of the provider.
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
`id-token` | Write | ID token got from the provider.
`refresh-token` | Write | Refresh token got from the provider.
### Extra scopes
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
### CA Certificates
You can use your self-signed certificates for the provider.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```

View File

@@ -11,12 +11,11 @@ import (
"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/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/mock_adaptors"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/auth"
)
// Run the integration tests of the credential plugin use-case.
@@ -51,7 +50,7 @@ func TestCmd_Run_CredentialPlugin(t *testing.T) {
var idToken string
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(gomock.Any()).
Do(func(out credentialplugin.Output) {
@@ -63,21 +62,21 @@ func TestCmd_Run_CredentialPlugin(t *testing.T) {
}
})
req := startBrowserRequest(t, ctx, nil)
runGetTokenCmd(t, ctx, req, credentialPluginInteraction,
runGetTokenCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
credentialPluginInteraction,
"--skip-open-browser",
"--listen-port", "0",
"--token-cache-dir", cacheDir,
"--oidc-issuer-url", serverURL,
"--oidc-client-id", "kubernetes",
)
req.wait()
})
}
func runGetTokenCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, interaction adaptors.CredentialPluginInteraction, args ...string) {
func runGetTokenCmd(t *testing.T, ctx context.Context, localServerReadyFunc auth.LocalServerReadyFunc, interaction credentialplugin.Interface, args ...string) {
t.Helper()
cmd := di.NewCmdForHeadless(logger.New(t), s, interaction)
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, interaction)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "get-token", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)

View File

@@ -1,27 +0,0 @@
package logger
import (
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
func New(t testingLogger) *logger.Logger {
b := &bridge{t}
return logger.NewWith(b, b)
}
type testingLogger interface {
Logf(format string, v ...interface{})
}
type bridge struct {
t testingLogger
}
func (b *bridge) Printf(format string, v ...interface{}) {
b.t.Logf(format, v...)
}
func (b *bridge) Output(calldepth int, s string) error {
b.t.Logf("%s", s)
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"crypto/tls"
"net/http"
"os"
"sync"
"testing"
"time"
@@ -16,9 +15,9 @@ import (
"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/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/usecases/auth"
)
var (
@@ -33,7 +32,7 @@ var (
// 3. Open a request for the local server.
// 4. Verify the kubeconfig.
//
func TestCmd_Run_Login(t *testing.T) {
func TestCmd_Run_Standalone(t *testing.T) {
timeout := 1 * time.Second
type testParameter struct {
@@ -75,9 +74,9 @@ func TestCmd_Run_Login(t *testing.T) {
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, p.clientTLSConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -106,7 +105,9 @@ func TestCmd_Run_Login(t *testing.T) {
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -133,7 +134,9 @@ func TestCmd_Run_Login(t *testing.T) {
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -164,7 +167,9 @@ func TestCmd_Run_Login(t *testing.T) {
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "NEW_REFRESH_TOKEN",
@@ -195,8 +200,9 @@ func TestCmd_Run_Login(t *testing.T) {
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, p.clientTLSConfig)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, p.clientTLSConfig),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -228,9 +234,9 @@ func TestCmd_Run_Login(t *testing.T) {
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
req.wait()
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
"--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -256,9 +262,9 @@ func TestCmd_Run_Login(t *testing.T) {
})
defer os.Remove(kubeConfigFilename)
req := startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
req.wait()
runCmd(t, ctx,
openBrowserOnReadyFunc(t, ctx, nil),
"--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "YOUR_REFRESH_TOKEN",
@@ -306,69 +312,34 @@ func setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, server
})
}
func runCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, args ...string) {
func runCmd(t *testing.T, ctx context.Context, localServerReadyFunc auth.LocalServerReadyFunc, args ...string) {
t.Helper()
cmd := di.NewCmdForHeadless(logger.New(t), s, nil)
cmd := di.NewCmdForHeadless(mock_logger.New(t), localServerReadyFunc, nil)
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
type nopBrowserRequest struct {
t *testing.T
}
func (r *nopBrowserRequest) ShowLocalServerURL(url string) {
r.t.Errorf("ShowLocalServerURL must not be called")
}
type browserRequest struct {
t *testing.T
urlCh chan<- string
wg *sync.WaitGroup
}
func (r *browserRequest) ShowLocalServerURL(url string) {
defer close(r.urlCh)
r.t.Logf("Open %s for authentication", url)
r.urlCh <- url
}
func (r *browserRequest) wait() {
r.wg.Wait()
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
t.Helper()
urlCh := make(chan string)
var wg sync.WaitGroup
go func() {
defer wg.Done()
select {
case url := <-urlCh:
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
case err := <-ctx.Done():
t.Errorf("context done while waiting for URL prompt: %s", err)
func openBrowserOnReadyFunc(t *testing.T, ctx context.Context, clientConfig *tls.Config) auth.LocalServerReadyFunc {
return func(url string) {
client := http.Client{Transport: &http.Transport{TLSClientConfig: clientConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
}()
wg.Add(1)
return &browserRequest{t, urlCh, &wg}
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}
}
func setenv(t *testing.T, key, value string) {

8
go.mod
View File

@@ -3,21 +3,23 @@ module github.com/int128/kubelogin
go 1.12
require (
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/coreos/go-oidc v2.1.0+incompatible
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda
github.com/go-test/deep v1.0.2
github.com/go-test/deep v1.0.3
github.com/golang/mock v1.3.1
github.com/google/wire v0.3.0
github.com/int128/oauth2cli v1.4.1
github.com/int128/oauth2cli v1.5.0
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
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719
k8s.io/client-go v0.0.0-20190620085101-78d2af792bab
k8s.io/klog v0.4.0
)

9
go.sum
View File

@@ -6,6 +6,8 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -17,8 +19,11 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg
github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -45,6 +50,8 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/int128/oauth2cli v1.4.1 h1:IsaYMafEDS1jyArxYdmksw+nMsNxiYCQzdkPj3QF9BY=
github.com/int128/oauth2cli v1.4.1/go.mod h1:CMJjyUSgKiobye1M/9byFACOjtB2LRo2mo7boklEKlI=
github.com/int128/oauth2cli v1.5.0 h1:EOBMCWfroql1hPqPhP+EtDhgO7y6ClFZ/NwJEpBCo1s=
github.com/int128/oauth2cli v1.5.0/go.mod h1:ivzuzt+k+bpwLI1Mb1bRq8PiBvwLBsO8L7tX2F9iKKA=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -138,6 +145,8 @@ k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aG
k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68=
k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ=
k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4=
k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=

View File

@@ -2,25 +2,26 @@ package cmd
import (
"context"
"fmt"
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
"k8s.io/client-go/util/homedir"
)
// Set provides an implementation and interface for Cmd.
var Set = wire.NewSet(
wire.Struct(new(Cmd), "*"),
wire.Bind(new(adaptors.Cmd), new(*Cmd)),
wire.Bind(new(Interface), new(*Cmd)),
wire.Struct(new(Root), "*"),
wire.Struct(new(GetToken), "*"),
)
type Interface interface {
Run(ctx context.Context, args []string, version string) int
}
const examples = ` # Login to the provider using the authorization code flow.
%[1]s
@@ -35,9 +36,9 @@ 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
Logger adaptors.Logger
Root *Root
GetToken *GetToken
Logger logger.Interface
}
// Run parses the command line arguments and executes the specified use-case.
@@ -45,12 +46,12 @@ type Cmd struct {
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
executable := filepath.Base(args[0])
rootCmd := newRootCmd(ctx, executable, cmd)
rootCmd := cmd.Root.New(ctx, executable)
rootCmd.Version = version
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
getTokenCmd := newGetTokenCmd(ctx, cmd)
getTokenCmd := cmd.GetToken.New(ctx)
rootCmd.AddCommand(getTokenCmd)
versionCmd := &cobra.Command{
@@ -66,146 +67,8 @@ func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
rootCmd.SetArgs(args[1:])
if err := rootCmd.Execute(); err != nil {
cmd.Logger.Printf("error: %s", err)
cmd.Logger.Debugf(1, "stacktrace: %+v", err)
cmd.Logger.V(1).Infof("stacktrace: %+v", err)
return 1
}
return 0
}
// kubectlOptions represents kubectl specific options.
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
}
func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
}
// loginOptions represents the options for Login use-case.
type loginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
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")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
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 {
loginOptions
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
TokenCacheDir string
}
func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
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")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
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.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
}
func newGetTokenCmd(ctx context.Context, cmd *Cmd) *cobra.Command {
var o getTokenOptions
c := &cobra.Command{
Use: "get-token [flags]",
Short: "Run as a kubectl credential plugin",
Args: func(c *cobra.Command, args []string) error {
if err := cobra.NoArgs(c, args); err != nil {
return err
}
if o.IssuerURL == "" {
return xerrors.New("--oidc-issuer-url is missing")
}
if o.ClientID == "" {
return xerrors.New("--oidc-client-id is missing")
}
return nil
},
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,
TokenCacheDir: o.TokenCacheDir,
}
if err := cmd.GetToken.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
}
return nil
},
}
o.register(c.Flags())
return c
}

View File

@@ -5,10 +5,11 @@ import (
"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"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/int128/kubelogin/pkg/usecases/standalone/mock_standalone"
)
func TestCmd_Run(t *testing.T) {
@@ -20,18 +21,18 @@ func TestCmd_Run(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
ListenPort: defaultListenPort,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
Login: login,
Logger: logger,
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
@@ -44,9 +45,9 @@ func TestCmd_Run(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
@@ -58,12 +59,12 @@ func TestCmd_Run(t *testing.T) {
Password: "PASS",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
Root: &Root{
Standalone: mockStandalone,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
@@ -87,8 +88,11 @@ func TestCmd_Run(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
@@ -101,21 +105,24 @@ func TestCmd_Run(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_usecases.NewMockGetToken(ctrl)
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, usecases.GetTokenIn{
Do(ctx, credentialplugin.Input{
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,
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
@@ -132,9 +139,9 @@ func TestCmd_Run(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_usecases.NewMockGetToken(ctrl)
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, usecases.GetTokenIn{
Do(ctx, credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
@@ -148,12 +155,15 @@ func TestCmd_Run(t *testing.T) {
Password: "PASS",
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
GetToken: getToken,
Logger: logger,
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable,
"get-token",
@@ -181,8 +191,14 @@ func TestCmd_Run(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
GetToken: mock_usecases.NewMockGetToken(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
@@ -195,8 +211,14 @@ func TestCmd_Run(t *testing.T) {
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
GetToken: mock_usecases.NewMockGetToken(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
Root: &Root{
Logger: mock_logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
},
Logger: mock_logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {

View File

@@ -0,0 +1,82 @@
package cmd
import (
"context"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// getTokenOptions represents the options for get-token command.
type getTokenOptions struct {
loginOptions
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CertificateAuthority string
SkipTLSVerify bool
Verbose int
TokenCacheDir string
}
func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
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")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
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.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
}
type GetToken struct {
GetToken credentialplugin.Interface
Logger logger.Interface
}
func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
var o getTokenOptions
c := &cobra.Command{
Use: "get-token [flags]",
Short: "Run as a kubectl credential plugin",
Args: func(c *cobra.Command, args []string) error {
if err := cobra.NoArgs(c, args); err != nil {
return err
}
if o.IssuerURL == "" {
return xerrors.New("--oidc-issuer-url is missing")
}
if o.ClientID == "" {
return xerrors.New("--oidc-client-id is missing")
}
return nil
},
RunE: func(*cobra.Command, []string) error {
in := credentialplugin.Input{
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)
}
return nil
},
}
o.register(c.Flags())
return c
}

86
pkg/adaptors/cmd/root.go Normal file
View File

@@ -0,0 +1,86 @@
package cmd
import (
"context"
"fmt"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
// kubectlOptions represents kubectl specific options.
type kubectlOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
}
func (o *kubectlOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
}
// loginOptions represents the options for Login use-case.
type loginOptions struct {
ListenPort []int
SkipOpenBrowser bool
Username string
Password string
}
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")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
type Root struct {
Standalone standalone.Interface
Logger logger.Interface
}
func (cmd *Root) New(ctx context.Context, executable string) *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 {
in := standalone.Input{
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.Standalone.Do(ctx, in); err != nil {
return xerrors.Errorf("error: %w", err)
}
return nil
},
}
o.kubectlOptions.register(rootCmd.Flags())
o.loginOptions.register(rootCmd.Flags())
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
return rootCmd
}

View File

@@ -4,24 +4,35 @@ package credentialplugin
import (
"encoding/json"
"os"
"time"
"github.com/google/wire"
"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"
)
//go:generate mockgen -destination mock_credentialplugin/mock_credentialplugin.go github.com/int128/kubelogin/pkg/adaptors/credentialplugin Interface
var Set = wire.NewSet(
wire.Struct(new(Interaction), "*"),
wire.Bind(new(adaptors.CredentialPluginInteraction), new(*Interaction)),
wire.Bind(new(Interface), new(*Interaction)),
)
type Interface interface {
Write(out Output) error
}
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}
type Interaction struct{}
// Write writes the ExecCredential to standard output for kubectl.
func (*Interaction) Write(out credentialplugin.Output) error {
func (*Interaction) Write(out Output) error {
ec := &v1beta1.ExecCredential{
TypeMeta: v1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",

View File

@@ -0,0 +1,46 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialplugin (interfaces: Interface)
// Package mock_credentialplugin is a generated GoMock package.
package mock_credentialplugin
import (
gomock "github.com/golang/mock/gomock"
credentialplugin "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Write mocks base method
func (m *MockInterface) Write(arg0 credentialplugin.Output) error {
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)
}

View File

@@ -6,17 +6,31 @@ import (
"syscall"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/pkg/browser"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_env/mock_env.go github.com/int128/kubelogin/pkg/adaptors/env Interface
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 Env.
var Set = wire.NewSet(
wire.Struct(new(Env), "*"),
wire.Bind(new(adaptors.Env), new(*Env)),
wire.Bind(new(Interface), new(*Env)),
)
type Interface interface {
ReadPassword(prompt string) (string, error)
OpenBrowser(url string) error
}
// Env provides environment specific facilities.
type Env struct{}
@@ -34,3 +48,11 @@ func (*Env) ReadPassword(prompt string) (string, error) {
}
return string(b), nil
}
// OpenBrowser opens the default browser.
func (env *Env) OpenBrowser(url string) error {
if err := browser.OpenURL(url); err != nil {
return xerrors.Errorf("could not open the browser: %w", err)
}
return nil
}

58
pkg/adaptors/env/mock_env/mock_env.go vendored Normal file
View File

@@ -0,0 +1,58 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/env (interfaces: Interface)
// Package mock_env is a generated GoMock package.
package mock_env
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// OpenBrowser mocks base method
func (m *MockInterface) OpenBrowser(arg0 string) error {
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// OpenBrowser indicates an expected call of OpenBrowser
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
}
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}

View File

@@ -1,102 +0,0 @@
package adaptors
import (
"context"
"time"
"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/pkg/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,OIDCDecoder,Env,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type Kubeconfig interface {
GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error)
UpdateAuthProvider(auth *kubeconfig.AuthProvider) error
}
type TokenCacheRepository interface {
FindByKey(dir string, key credentialplugin.TokenCacheKey) (*credentialplugin.TokenCache, error)
Save(dir string, key credentialplugin.TokenCacheKey, cache credentialplugin.TokenCache) error
}
type CredentialPluginInteraction interface {
Write(out credentialplugin.Output) error
}
type OIDC interface {
New(ctx context.Context, config OIDCClientConfig) (OIDCClient, error)
}
// OIDCClientConfig represents a configuration of an OIDCClient to create.
type OIDCClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
SkipTLSVerify bool
}
type OIDCClient interface {
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
Refresh(ctx context.Context, in OIDCRefreshIn) (*OIDCAuthenticateOut, error)
}
// OIDCAuthenticateByCodeIn represents an input DTO of OIDCClient.AuthenticateByCode.
type OIDCAuthenticateByCodeIn struct {
LocalServerPort []int // HTTP server port candidates
SkipOpenBrowser bool // skip opening browser if true
ShowLocalServerURL interface{ ShowLocalServerURL(url string) }
}
// OIDCAuthenticateByPasswordIn represents an input DTO of OIDCClient.AuthenticateByPassword.
type OIDCAuthenticateByPasswordIn struct {
Username string
Password string
}
// OIDCAuthenticateOut represents an output DTO of
// OIDCClient.AuthenticateByCode, OIDCClient.AuthenticateByPassword and OIDCClient.Refresh.
type OIDCAuthenticateOut struct {
IDToken string
RefreshToken string
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)
}
type Logger interface {
Printf(format string, v ...interface{})
Debugf(level LogLevel, format string, v ...interface{})
SetLevel(level LogLevel)
IsEnabled(level LogLevel) bool
}
// LogLevel represents a log level for debug.
//
// 0 = None
// 1 = Including in/out
// 2 = Including transport headers
// 3 = Including transport body
//
type LogLevel int

View File

@@ -2,13 +2,46 @@ package kubeconfig
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
)
//go:generate mockgen -destination mock_kubeconfig/mock_kubeconfig.go github.com/int128/kubelogin/pkg/adaptors/kubeconfig Interface
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Kubeconfig), "*"),
wire.Bind(new(adaptors.Kubeconfig), new(*Kubeconfig)),
wire.Bind(new(Interface), new(*Kubeconfig)),
)
type Interface interface {
GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error)
UpdateAuthProvider(auth *AuthProvider) error
}
// ContextName represents name of a context.
type ContextName string
// UserName represents name of a user.
type UserName string
// AuthProvider represents the authentication provider,
// i.e. context, user and auth-provider in a kubeconfig.
type AuthProvider struct {
LocationOfOrigin string // Path to the kubeconfig file which contains the user
UserName UserName // User name
ContextName ContextName // Context name (optional)
OIDCConfig OIDCConfig
}
// OIDCConfig represents a configuration of an OIDC provider.
type OIDCConfig struct {
IDPIssuerURL string // idp-issuer-url
ClientID string // client-id
ClientSecret string // client-secret
IDPCertificateAuthority string // (optional) idp-certificate-authority
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
ExtraScopes []string // (optional) extra-scopes
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
}
type Kubeconfig struct{}

View File

@@ -3,13 +3,12 @@ package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, xerrors.Errorf("could not load kubeconfig: %w", err)
@@ -35,16 +34,16 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
func findCurrentAuthProvider(config *api.Config, contextName ContextName, userName UserName) (*AuthProvider, error) {
if userName == "" {
if contextName == "" {
contextName = kubeconfig.ContextName(config.CurrentContext)
contextName = ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, xerrors.Errorf("context %s does not exist", contextName)
}
userName = kubeconfig.UserName(contextNode.AuthInfo)
userName = UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
@@ -59,7 +58,7 @@ func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextN
if userNode.AuthProvider.Config == nil {
return nil, xerrors.New("auth-provider.config is missing")
}
return &kubeconfig.AuthProvider{
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
@@ -67,12 +66,12 @@ func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextN
}, nil
}
func makeOIDCConfig(m map[string]string) kubeconfig.OIDCConfig {
func makeOIDCConfig(m map[string]string) OIDCConfig {
var extraScopes []string
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return kubeconfig.OIDCConfig{
return OIDCConfig{
IDPIssuerURL: m["idp-issuer-url"],
ClientID: m["client-id"],
ClientSecret: m["client-secret"],

View File

@@ -5,7 +5,6 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
@@ -106,11 +105,11 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.AuthProvider{
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: kubeconfig.OIDCConfig{
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
@@ -148,11 +147,11 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.AuthProvider{
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
OIDCConfig: kubeconfig.OIDCConfig{
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
}
@@ -178,10 +177,10 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.AuthProvider{
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
OIDCConfig: kubeconfig.OIDCConfig{
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
},
}

View File

@@ -0,0 +1,59 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/kubeconfig (interfaces: Interface)
// Package mock_kubeconfig is a generated GoMock package.
package mock_kubeconfig
import (
gomock "github.com/golang/mock/gomock"
kubeconfig "github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// GetCurrentAuthProvider mocks base method
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuthProvider mocks base method
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
}

View File

@@ -3,12 +3,11 @@ package kubeconfig
import (
"strings"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
)
func (*Kubeconfig) UpdateAuthProvider(auth *kubeconfig.AuthProvider) error {
func (*Kubeconfig) UpdateAuthProvider(auth *AuthProvider) error {
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
if err != nil {
return xerrors.Errorf("could not load %s: %w", auth.LocationOfOrigin, err)
@@ -30,7 +29,7 @@ func (*Kubeconfig) UpdateAuthProvider(auth *kubeconfig.AuthProvider) error {
return nil
}
func copyOIDCConfig(config kubeconfig.OIDCConfig, m map[string]string) {
func copyOIDCConfig(config OIDCConfig, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", config.IDPIssuerURL)
setOrDeleteKey(m, "client-id", config.ClientID)
setOrDeleteKey(m, "client-secret", config.ClientSecret)

View File

@@ -4,8 +4,6 @@ import (
"io/ioutil"
"os"
"testing"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
@@ -18,10 +16,10 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuthProvider(&kubeconfig.AuthProvider{
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",
@@ -66,10 +64,10 @@ users:
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuthProvider(&kubeconfig.AuthProvider{
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
OIDCConfig: kubeconfig.OIDCConfig{
OIDCConfig: OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
ClientSecret: "GOOGLE_CLIENT_SECRET",

View File

@@ -1,12 +1,13 @@
package logger
import (
"fmt"
"flag"
"log"
"os"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/spf13/pflag"
"k8s.io/klog"
)
// Set provides an implementation and interface for Logger.
@@ -14,43 +15,46 @@ var Set = wire.NewSet(
New,
)
// New returns a Logger with the standard log.Logger for messages and debug.
func New() adaptors.Logger {
// New returns a Logger with the standard log.Logger and klog.
func New() Interface {
return &Logger{
stdLogger: log.New(os.Stderr, "", 0),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds|log.Lshortfile),
goLogger: log.New(os.Stderr, "", 0),
}
}
func NewWith(s stdLogger, d debugLogger) *Logger {
return &Logger{s, d, 0}
type Interface interface {
AddFlags(f *pflag.FlagSet)
Printf(format string, args ...interface{})
V(level int) Verbose
IsEnabled(level int) bool
}
type stdLogger interface {
type Verbose interface {
Infof(format string, args ...interface{})
}
type goLogger interface {
Printf(format string, v ...interface{})
}
type debugLogger interface {
Output(calldepth int, s string) error
}
// Logger wraps the standard log.Logger and just provides debug level.
// Logger provides logging facility using log.Logger and klog.
type Logger struct {
stdLogger
debugLogger
level adaptors.LogLevel
goLogger
}
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
if l.IsEnabled(level) {
_ = l.debugLogger.Output(2, fmt.Sprintf(format, v...))
}
// AddFlags adds the flags such as -v.
func (*Logger) AddFlags(f *pflag.FlagSet) {
gf := flag.NewFlagSet("", flag.ContinueOnError)
klog.InitFlags(gf)
f.AddGoFlagSet(gf)
}
func (l *Logger) SetLevel(level adaptors.LogLevel) {
l.level = level
// V returns a logger enabled only if the level is enabled.
func (*Logger) V(level int) Verbose {
return klog.V(klog.Level(level))
}
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
return level <= l.level
// IsEnabled returns true if the level is enabled.
func (*Logger) IsEnabled(level int) bool {
return bool(klog.V(klog.Level(level)))
}

View File

@@ -1,62 +0,0 @@
package logger
import (
"fmt"
"testing"
"github.com/int128/kubelogin/pkg/adaptors"
)
type mockDebugLogger struct {
count int
}
func (l *mockDebugLogger) Output(int, string) error {
l.count++
return nil
}
func TestLogger_Debugf(t *testing.T) {
for _, c := range []struct {
loggerLevel adaptors.LogLevel
debugfLevel adaptors.LogLevel
count int
}{
{0, 0, 1},
{0, 1, 0},
{1, 0, 1},
{1, 1, 1},
{1, 2, 0},
{2, 1, 1},
{2, 2, 1},
{2, 3, 0},
} {
t.Run(fmt.Sprintf("%+v", c), func(t *testing.T) {
m := &mockDebugLogger{}
l := &Logger{debugLogger: m, level: c.loggerLevel}
l.Debugf(c.debugfLevel, "hello")
if m.count != c.count {
t.Errorf("count wants %d but %d", c.count, m.count)
}
})
}
}
type mockStdLogger struct {
count int
}
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
l.count++
}
func TestLogger_Printf(t *testing.T) {
m := &mockStdLogger{}
l := &Logger{stdLogger: m}
l.Printf("hello")
if m.count != 1 {
t.Errorf("count wants %d but %d", 1, m.count)
}
}

View File

@@ -0,0 +1,46 @@
package mock_logger
import (
"fmt"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/spf13/pflag"
)
func New(t testingLogger) *Logger {
return &Logger{t}
}
type testingLogger interface {
Logf(format string, v ...interface{})
}
// Logger provides logging facility using testing.T.
type Logger struct {
t testingLogger
}
func (*Logger) AddFlags(f *pflag.FlagSet) {
f.IntP("v", "v", 0, "dummy flag used in the tests")
}
func (l *Logger) Printf(format string, args ...interface{}) {
l.t.Logf(format, args...)
}
type Verbose struct {
t testingLogger
level int
}
func (v *Verbose) Infof(format string, args ...interface{}) {
v.t.Logf(fmt.Sprintf("I%d] ", v.level)+format, args...)
}
func (l *Logger) V(level int) logger.Verbose {
return &Verbose{l.t, level}
}
func (*Logger) IsEnabled(level int) bool {
return true
}

View File

@@ -1,31 +0,0 @@
package mock_adaptors
import (
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/adaptors"
)
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {
return &Logger{
MockLogger: NewMockLogger(ctrl),
testingLogger: t,
}
}
type testingLogger interface {
Logf(format string, v ...interface{})
}
// Logger provides mock feature but overrides output methods with actual logging.
type Logger struct {
*MockLogger
testingLogger testingLogger
}
func (l *Logger) Printf(format string, v ...interface{}) {
l.testingLogger.Logf(format, v...)
}
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
l.testingLogger.Logf(format, v...)
}

View File

@@ -1,390 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// 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
import (
context "context"
gomock "github.com/golang/mock/gomock"
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"
)
// MockKubeconfig is a mock of Kubeconfig interface
type MockKubeconfig struct {
ctrl *gomock.Controller
recorder *MockKubeconfigMockRecorder
}
// MockKubeconfigMockRecorder is the mock recorder for MockKubeconfig
type MockKubeconfigMockRecorder struct {
mock *MockKubeconfig
}
// NewMockKubeconfig creates a new mock instance
func NewMockKubeconfig(ctrl *gomock.Controller) *MockKubeconfig {
mock := &MockKubeconfig{ctrl: ctrl}
mock.recorder = &MockKubeconfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockKubeconfig) EXPECT() *MockKubeconfigMockRecorder {
return m.recorder
}
// GetCurrentAuthProvider mocks base method
func (m *MockKubeconfig) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
func (mr *MockKubeconfigMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
}
// UpdateAuthProvider mocks base method
func (m *MockKubeconfig) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
func (mr *MockKubeconfigMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuthProvider), arg0)
}
// MockTokenCacheRepository is a mock of TokenCacheRepository interface
type MockTokenCacheRepository struct {
ctrl *gomock.Controller
recorder *MockTokenCacheRepositoryMockRecorder
}
// MockTokenCacheRepositoryMockRecorder is the mock recorder for MockTokenCacheRepository
type MockTokenCacheRepositoryMockRecorder struct {
mock *MockTokenCacheRepository
}
// NewMockTokenCacheRepository creates a new mock instance
func NewMockTokenCacheRepository(ctrl *gomock.Controller) *MockTokenCacheRepository {
mock := &MockTokenCacheRepository{ctrl: ctrl}
mock.recorder = &MockTokenCacheRepositoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockTokenCacheRepository) EXPECT() *MockTokenCacheRepositoryMockRecorder {
return m.recorder
}
// 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
}
// 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)
}
// 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
}
// 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
type MockCredentialPluginInteraction struct {
ctrl *gomock.Controller
recorder *MockCredentialPluginInteractionMockRecorder
}
// MockCredentialPluginInteractionMockRecorder is the mock recorder for MockCredentialPluginInteraction
type MockCredentialPluginInteractionMockRecorder struct {
mock *MockCredentialPluginInteraction
}
// NewMockCredentialPluginInteraction creates a new mock instance
func NewMockCredentialPluginInteraction(ctrl *gomock.Controller) *MockCredentialPluginInteraction {
mock := &MockCredentialPluginInteraction{ctrl: ctrl}
mock.recorder = &MockCredentialPluginInteractionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockCredentialPluginInteraction) EXPECT() *MockCredentialPluginInteractionMockRecorder {
return m.recorder
}
// Write mocks base method
func (m *MockCredentialPluginInteraction) Write(arg0 credentialplugin.Output) error {
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Write indicates an expected call of Write
func (mr *MockCredentialPluginInteractionMockRecorder) Write(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockCredentialPluginInteraction)(nil).Write), arg0)
}
// MockOIDC is a mock of OIDC interface
type MockOIDC struct {
ctrl *gomock.Controller
recorder *MockOIDCMockRecorder
}
// MockOIDCMockRecorder is the mock recorder for MockOIDC
type MockOIDCMockRecorder struct {
mock *MockOIDC
}
// NewMockOIDC creates a new mock instance
func NewMockOIDC(ctrl *gomock.Controller) *MockOIDC {
mock := &MockOIDC{ctrl: ctrl}
mock.recorder = &MockOIDCMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockOIDC) New(arg0 context.Context, arg1 adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(adaptors.OIDCClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockOIDCMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOIDC)(nil).New), arg0, arg1)
}
// MockOIDCClient is a mock of OIDCClient interface
type MockOIDCClient struct {
ctrl *gomock.Controller
recorder *MockOIDCClientMockRecorder
}
// MockOIDCClientMockRecorder is the mock recorder for MockOIDCClient
type MockOIDCClientMockRecorder struct {
mock *MockOIDCClient
}
// NewMockOIDCClient creates a new mock instance
func NewMockOIDCClient(ctrl *gomock.Controller) *MockOIDCClient {
mock := &MockOIDCClient{ctrl: ctrl}
mock.recorder = &MockOIDCClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDCClient) EXPECT() *MockOIDCClientMockRecorder {
return m.recorder
}
// AuthenticateByCode mocks base method
func (m *MockOIDCClient) AuthenticateByCode(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByCode indicates an expected call of AuthenticateByCode
func (mr *MockOIDCClientMockRecorder) AuthenticateByCode(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByCode), arg0, arg1)
}
// AuthenticateByPassword mocks base method
func (m *MockOIDCClient) AuthenticateByPassword(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
func (mr *MockOIDCClientMockRecorder) AuthenticateByPassword(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByPassword), arg0, arg1)
}
// Refresh mocks base method
func (m *MockOIDCClient) Refresh(arg0 context.Context, arg1 adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockOIDCClientMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockOIDCClient)(nil).Refresh), arg0, arg1)
}
// 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
}
// 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
type MockEnv struct {
ctrl *gomock.Controller
recorder *MockEnvMockRecorder
}
// MockEnvMockRecorder is the mock recorder for MockEnv
type MockEnvMockRecorder struct {
mock *MockEnv
}
// NewMockEnv creates a new mock instance
func NewMockEnv(ctrl *gomock.Controller) *MockEnv {
mock := &MockEnv{ctrl: ctrl}
mock.recorder = &MockEnvMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockEnv) EXPECT() *MockEnvMockRecorder {
return m.recorder
}
// ReadPassword mocks base method
func (m *MockEnv) ReadPassword(arg0 string) (string, error) {
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockEnvMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockEnv)(nil).ReadPassword), arg0)
}
// MockLogger is a mock of Logger interface
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debugf mocks base method
func (m *MockLogger) Debugf(arg0 adaptors.LogLevel, arg1 string, arg2 ...interface{}) {
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf
func (mr *MockLoggerMockRecorder) Debugf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// IsEnabled mocks base method
func (m *MockLogger) IsEnabled(arg0 adaptors.LogLevel) bool {
ret := m.ctrl.Call(m, "IsEnabled", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsEnabled indicates an expected call of IsEnabled
func (mr *MockLoggerMockRecorder) IsEnabled(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockLogger)(nil).IsEnabled), arg0)
}
// Printf mocks base method
func (m *MockLogger) Printf(arg0 string, arg1 ...interface{}) {
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Printf", varargs...)
}
// Printf indicates an expected call of Printf
func (mr *MockLoggerMockRecorder) Printf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Printf", reflect.TypeOf((*MockLogger)(nil).Printf), varargs...)
}
// SetLevel mocks base method
func (m *MockLogger) SetLevel(arg0 adaptors.LogLevel) {
m.ctrl.Call(m, "SetLevel", arg0)
}
// SetLevel indicates an expected call of SetLevel
func (mr *MockLoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*MockLogger)(nil).SetLevel), arg0)
}

161
pkg/adaptors/oidc/client.go Normal file
View File

@@ -0,0 +1,161 @@
package oidc
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/http"
"time"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type Interface interface {
AuthenticateByCode(ctx context.Context, localServerPort []int, localServerReadyChan chan<- string) (*TokenSet, error)
AuthenticateByPassword(ctx context.Context, username, password string) (*TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
}
// TokenSet represents an output DTO of
// Interface.AuthenticateByCode, Interface.AuthenticateByPassword and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger logger.Interface
}
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, localServerPort []int, localServerReadyChan chan<- string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
nonce, err := newNonce()
if err != nil {
return nil, xerrors.Errorf("could not generate a nonce parameter")
}
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
LocalServerPort: localServerPort,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
LocalServerReadyChan: localServerReadyChan,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
if verifiedIDToken.Nonce != nonce {
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func newNonce() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", xerrors.Errorf("error while reading random: %w", err)
}
return fmt.Sprintf("%x", n), nil
}
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(ctx context.Context, username, password string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, 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.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
RefreshToken: refreshToken,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, 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.V(1).Infof("incomplete claims of the ID token: %w", err)
}
return &TokenSet{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
return dumpRawClaims(rawClaims), err
}

View File

@@ -8,15 +8,23 @@ import (
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/xerrors"
)
type DecoderInterface interface {
DecodeIDToken(t string) (*DecodedIDToken, error)
}
type DecodedIDToken struct {
IDTokenExpiry time.Time
IDTokenClaims map[string]string // string representation of claims for logging
}
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) {
func (d *Decoder) DecodeIDToken(t string) (*DecodedIDToken, error) {
parts := strings.Split(t, ".")
if len(parts) != 3 {
return nil, xerrors.Errorf("token contains an invalid number of segments")
@@ -33,7 +41,7 @@ func (d *Decoder) DecodeIDToken(t string) (*adaptors.DecodedIDToken, error) {
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{
return &DecodedIDToken{
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
IDTokenClaims: dumpRawClaims(rawClaims),
}, nil

View File

@@ -0,0 +1,122 @@
package oidc
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
type FactoryInterface interface {
New(ctx context.Context, config ClientConfig) (Interface, error)
}
// ClientConfig represents a configuration of an Interface to create.
type ClientConfig struct {
Config kubeconfig.OIDCConfig
CACertFilename string
SkipTLSVerify bool
}
type Factory struct {
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, config ClientConfig) (Interface, error) {
tlsConfig, err := f.tlsConfigFor(config)
if err != nil {
return nil, xerrors.Errorf("could not initialize TLS config: %w", err)
}
baseTransport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.Config.IDPIssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDCFactory issuer: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.Config.ClientID,
ClientSecret: config.Config.ClientSecret,
Scopes: append(config.Config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
}, nil
}
func (f *Factory) tlsConfigFor(config ClientConfig) (*tls.Config, error) {
pool := x509.NewCertPool()
if config.Config.IDPCertificateAuthority != "" {
f.Logger.V(1).Infof("loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if config.Config.IDPCertificateAuthorityData != "" {
f.Logger.V(1).Infof("loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if config.CACertFilename != "" {
f.Logger.V(1).Infof("loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
c := &tls.Config{
InsecureSkipVerify: config.SkipTLSVerify,
}
if len(pool.Subjects()) > 0 {
c.RootCAs = pool
}
return c, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -1,20 +1,19 @@
package tls
package oidc
import (
"io/ioutil"
"testing"
"github.com/int128/kubelogin/e2e_test/logger"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
)
func TestNewConfig(t *testing.T) {
testingLogger := logger.New(t)
testingLogger.SetLevel(1)
func TestFactory_tlsConfigFor(t *testing.T) {
testingLogger := mock_logger.New(t)
factory := &Factory{Logger: testingLogger}
t.Run("Defaults", func(t *testing.T) {
c, err := NewConfig(adaptors.OIDCClientConfig{}, testingLogger)
c, err := factory.tlsConfigFor(ClientConfig{})
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
@@ -26,10 +25,10 @@ func TestNewConfig(t *testing.T) {
}
})
t.Run("SkipTLSVerify", func(t *testing.T) {
config := adaptors.OIDCClientConfig{
config := ClientConfig{
SkipTLSVerify: true,
}
c, err := NewConfig(config, testingLogger)
c, err := factory.tlsConfigFor(config)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
@@ -41,14 +40,14 @@ func TestNewConfig(t *testing.T) {
}
})
t.Run("AllCertificates", func(t *testing.T) {
config := adaptors.OIDCClientConfig{
config := ClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/ca2.crt.base64")),
IDPCertificateAuthority: "testdata/tls/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/tls/ca2.crt.base64")),
},
CACertFilename: "testdata/ca3.crt",
CACertFilename: "testdata/tls/ca3.crt",
}
c, err := NewConfig(config, testingLogger)
c, err := factory.tlsConfigFor(config)
if err != nil {
t.Fatalf("NewConfig error: %+v", err)
}
@@ -64,14 +63,14 @@ func TestNewConfig(t *testing.T) {
}
})
t.Run("InvalidCertificate", func(t *testing.T) {
config := adaptors.OIDCClientConfig{
config := ClientConfig{
Config: kubeconfig.OIDCConfig{
IDPCertificateAuthority: "testdata/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/ca2.crt.base64")),
IDPCertificateAuthority: "testdata/tls/ca1.crt",
IDPCertificateAuthorityData: string(readFile(t, "testdata/tls/ca2.crt.base64")),
},
CACertFilename: "testdata/Makefile", // invalid cert
}
_, err := NewConfig(config, testingLogger)
_, err := factory.tlsConfigFor(config)
if err == nil {
t.Fatalf("NewConfig wants non-nil but nil")
}

View File

@@ -4,47 +4,39 @@ import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
const (
logLevelDumpHeaders = 2
logLevelDumpBody = 3
levelDumpHeaders = 2
levelDumpBody = 3
)
type Transport struct {
Base http.RoundTripper
Logger adaptors.Logger
Logger logger.Interface
}
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.IsDumpEnabled() {
if !t.Logger.IsEnabled(levelDumpHeaders) {
return t.Base.RoundTrip(req)
}
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
reqDump, err := httputil.DumpRequestOut(req, t.Logger.IsEnabled(levelDumpBody))
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the request: %s", err)
t.Logger.V(levelDumpHeaders).Infof("could not dump the request: %s", err)
return t.Base.RoundTrip(req)
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
t.Logger.V(levelDumpHeaders).Infof("%s", string(reqDump))
resp, err := t.Base.RoundTrip(req)
if err != nil {
return resp, err
}
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
respDump, err := httputil.DumpResponse(resp, t.Logger.IsEnabled(levelDumpBody))
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "could not dump the response: %s", err)
t.Logger.V(levelDumpHeaders).Infof("could not dump the response: %s", err)
return resp, err
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
t.Logger.V(levelDumpHeaders).Infof("%s", string(respDump))
return resp, err
}
func (t *Transport) IsDumpEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpHeaders)
}
func (t *Transport) IsDumpBodyEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpBody)
}

View File

@@ -8,8 +8,7 @@ import (
"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/adaptors/logger/mock_logger"
)
type mockTransport struct {
@@ -26,12 +25,6 @@ func TestLoggingTransport_RoundTrip(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(gomock.Any()).
Return(true).
AnyTimes()
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
Host: example.com
@@ -44,7 +37,7 @@ dummy`)), req)
transport := &Transport{
Base: &mockTransport{resp: resp},
Logger: logger,
Logger: mock_logger.New(t),
}
gotResp, err := transport.RoundTrip(req)
if err != nil {
@@ -54,37 +47,3 @@ dummy`)), req)
t.Errorf("resp wants %v but %v", resp, gotResp)
}
}
func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
Return(true)
transport := &Transport{
Logger: logger,
}
if transport.IsDumpEnabled() != true {
t.Errorf("IsDumpEnabled wants true")
}
}
func TestLoggingTransport_IsDumpBodyEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(adaptors.LogLevel(logLevelDumpBody)).
Return(true)
transport := &Transport{
Logger: logger,
}
if transport.IsDumpBodyEnabled() != true {
t.Errorf("IsDumpBodyEnabled wants true")
}
}

View File

@@ -0,0 +1,146 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidc (interfaces: FactoryInterface,Interface,DecoderInterface)
// Package mock_oidc is a generated GoMock package.
package mock_oidc
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidc "github.com/int128/kubelogin/pkg/adaptors/oidc"
reflect "reflect"
)
// MockFactoryInterface is a mock of FactoryInterface interface
type MockFactoryInterface struct {
ctrl *gomock.Controller
recorder *MockFactoryInterfaceMockRecorder
}
// MockFactoryInterfaceMockRecorder is the mock recorder for MockFactoryInterface
type MockFactoryInterfaceMockRecorder struct {
mock *MockFactoryInterface
}
// NewMockFactoryInterface creates a new mock instance
func NewMockFactoryInterface(ctrl *gomock.Controller) *MockFactoryInterface {
mock := &MockFactoryInterface{ctrl: ctrl}
mock.recorder = &MockFactoryInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFactoryInterface) EXPECT() *MockFactoryInterfaceMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockFactoryInterface) New(arg0 context.Context, arg1 oidc.ClientConfig) (oidc.Interface, error) {
ret := m.ctrl.Call(m, "New", arg0, arg1)
ret0, _ := ret[0].(oidc.Interface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockFactoryInterfaceMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockFactoryInterface)(nil).New), arg0, arg1)
}
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// AuthenticateByCode mocks base method
func (m *MockInterface) AuthenticateByCode(arg0 context.Context, arg1 []int, arg2 chan<- string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByCode indicates an expected call of AuthenticateByCode
func (mr *MockInterfaceMockRecorder) AuthenticateByCode(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockInterface)(nil).AuthenticateByCode), arg0, arg1, arg2)
}
// AuthenticateByPassword mocks base method
func (m *MockInterface) AuthenticateByPassword(arg0 context.Context, arg1, arg2 string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
func (mr *MockInterfaceMockRecorder) AuthenticateByPassword(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockInterface)(nil).AuthenticateByPassword), arg0, arg1, arg2)
}
// Refresh mocks base method
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidc.TokenSet, error) {
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidc.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}
// MockDecoderInterface is a mock of DecoderInterface interface
type MockDecoderInterface struct {
ctrl *gomock.Controller
recorder *MockDecoderInterfaceMockRecorder
}
// MockDecoderInterfaceMockRecorder is the mock recorder for MockDecoderInterface
type MockDecoderInterfaceMockRecorder struct {
mock *MockDecoderInterface
}
// NewMockDecoderInterface creates a new mock instance
func NewMockDecoderInterface(ctrl *gomock.Controller) *MockDecoderInterface {
mock := &MockDecoderInterface{ctrl: ctrl}
mock.recorder = &MockDecoderInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDecoderInterface) EXPECT() *MockDecoderInterfaceMockRecorder {
return m.recorder
}
// DecodeIDToken mocks base method
func (m *MockDecoderInterface) DecodeIDToken(arg0 string) (*oidc.DecodedIDToken, error) {
ret := m.ctrl.Call(m, "DecodeIDToken", arg0)
ret0, _ := ret[0].(*oidc.DecodedIDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DecodeIDToken indicates an expected call of DecodeIDToken
func (mr *MockDecoderInterfaceMockRecorder) DecodeIDToken(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeIDToken", reflect.TypeOf((*MockDecoderInterface)(nil).DecodeIDToken), arg0)
}

View File

@@ -1,207 +1,15 @@
package oidc
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net/http"
"os"
"time"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"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
}
//go:generate mockgen -destination mock_oidc/mock_oidc.go github.com/int128/kubelogin/pkg/adaptors/oidc FactoryInterface,Interface,DecoderInterface
// Set provides an implementation and interface for OIDC.
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(adaptors.OIDC), new(*Factory)),
wire.Bind(new(FactoryInterface), new(*Factory)),
wire.Struct(new(Decoder)),
wire.Bind(new(adaptors.OIDCDecoder), new(*Decoder)),
wire.Bind(new(DecoderInterface), new(*Decoder)),
)
type Factory struct {
Logger adaptors.Logger
}
// New returns an instance of adaptors.OIDCClient with the given configuration.
func (f *Factory) New(ctx context.Context, config adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
tlsConfig, err := tls.NewConfig(config, f.Logger)
if err != nil {
return nil, xerrors.Errorf("could not initialize TLS config: %w", err)
}
baseTransport := &http.Transport{
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.Config.IDPIssuerURL)
if err != nil {
return nil, xerrors.Errorf("could not discovery the OIDC issuer: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.Config.ClientID,
ClientSecret: config.Config.ClientSecret,
Scopes: append(config.Config.ExtraScopes, oidc.ScopeOpenID),
},
logger: f.Logger,
}, nil
}
type client struct {
httpClient *http.Client
provider *oidc.Provider
oauth2Config oauth2.Config
logger adaptors.Logger
}
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")
}
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
ShowLocalServerURL: in.ShowLocalServerURL.ShowLocalServerURL,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, xerrors.Errorf("could not get a token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
}
if verifiedIDToken.Nonce != nonce {
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
}
claims, err := dumpClaims(verifiedIDToken)
if err != nil {
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
}
return &adaptors.OIDCAuthenticateOut{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func newNonce() (string, error) {
var n uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
return "", xerrors.Errorf("error while reading random: %w", err)
}
return fmt.Sprintf("%x", n), nil
}
// AuthenticateByPassword performs the resource owner password credentials flow.
func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
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)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, 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.OIDCAuthenticateOut{
IDToken: idToken,
RefreshToken: token.RefreshToken,
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,
}
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, 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.OIDCAuthenticateOut{
IDToken: idToken,
RefreshToken: token.RefreshToken,
IDTokenExpiry: verifiedIDToken.Expiry,
IDTokenClaims: claims,
}, nil
}
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
var rawClaims map[string]interface{}
err := token.Claims(&rawClaims)
return dumpRawClaims(rawClaims), err
}

View File

@@ -1,66 +0,0 @@
package tls
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"github.com/int128/kubelogin/pkg/adaptors"
"golang.org/x/xerrors"
)
// NewConfig returns a tls.Config with the given certificates and options.
func NewConfig(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*tls.Config, error) {
pool := x509.NewCertPool()
if config.Config.IDPCertificateAuthority != "" {
logger.Debugf(1, "Loading the certificate %s", config.Config.IDPCertificateAuthority)
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
}
}
if config.Config.IDPCertificateAuthorityData != "" {
logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
}
}
if config.CACertFilename != "" {
logger.Debugf(1, "Loading the certificate %s", config.CACertFilename)
err := appendCertificateFromFile(pool, config.CACertFilename)
if err != nil {
return nil, xerrors.Errorf("could not load the certificate: %w", err)
}
}
c := &tls.Config{
InsecureSkipVerify: config.SkipTLSVerify,
}
if len(pool.Subjects()) > 0 {
c.RootCAs = pool
}
return c, nil
}
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,59 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/tokencache (interfaces: Interface)
// Package mock_tokencache is a generated GoMock package.
package mock_tokencache
import (
gomock "github.com/golang/mock/gomock"
tokencache "github.com/int128/kubelogin/pkg/adaptors/tokencache"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// FindByKey mocks base method
func (m *MockInterface) FindByKey(arg0 string, arg1 tokencache.Key) (*tokencache.TokenCache, error) {
ret := m.ctrl.Call(m, "FindByKey", arg0, arg1)
ret0, _ := ret[0].(*tokencache.TokenCache)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindByKey indicates an expected call of FindByKey
func (mr *MockInterfaceMockRecorder) FindByKey(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByKey", reflect.TypeOf((*MockInterface)(nil).FindByKey), arg0, arg1)
}
// Save mocks base method
func (m *MockInterface) Save(arg0 string, arg1 tokencache.Key, arg2 tokencache.TokenCache) error {
ret := m.ctrl.Call(m, "Save", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// Save indicates an expected call of Save
func (mr *MockInterfaceMockRecorder) Save(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockInterface)(nil).Save), arg0, arg1, arg2)
}

View File

@@ -8,22 +8,39 @@ import (
"path/filepath"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_tokencache/mock_tokencache.go github.com/int128/kubelogin/pkg/adaptors/tokencache Interface
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Repository), "*"),
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
wire.Bind(new(Interface), new(*Repository)),
)
type Interface interface {
FindByKey(dir string, key Key) (*TokenCache, error)
Save(dir string, key Key, cache TokenCache) error
}
// Key represents a key of a token cache.
type Key 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"`
}
// 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) {
func (r *Repository) FindByKey(dir string, key Key) (*TokenCache, error) {
filename := filepath.Join(dir, computeFilename(key))
f, err := os.Open(filename)
if err != nil {
@@ -31,14 +48,14 @@ func (r *Repository) FindByKey(dir string, key credentialplugin.TokenCacheKey) (
}
defer f.Close()
d := json.NewDecoder(f)
var c credentialplugin.TokenCache
var c 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 {
func (r *Repository) Save(dir string, key Key, cache TokenCache) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return xerrors.Errorf("could not create directory %s: %w", dir, err)
}
@@ -55,7 +72,7 @@ func (r *Repository) Save(dir string, key credentialplugin.TokenCacheKey, cache
return nil
}
func computeFilename(key credentialplugin.TokenCacheKey) string {
func computeFilename(key Key) string {
s := sha256.New()
_, _ = s.Write([]byte(key.IssuerURL))
_, _ = s.Write([]byte{0x00})

View File

@@ -7,7 +7,6 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/int128/kubelogin/pkg/models/credentialplugin"
)
func TestRepository_FindByKey(t *testing.T) {
@@ -23,7 +22,7 @@ func TestRepository_FindByKey(t *testing.T) {
t.Errorf("could not clean up the temp dir: %s", err)
}
}()
key := credentialplugin.TokenCacheKey{
key := Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
}
@@ -37,7 +36,7 @@ func TestRepository_FindByKey(t *testing.T) {
if err != nil {
t.Errorf("err wants nil but %+v", err)
}
want := &credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
want := &TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := deep.Equal(tokenCache, want); diff != nil {
t.Error(diff)
}
@@ -58,11 +57,11 @@ func TestRepository_Save(t *testing.T) {
}
}()
key := credentialplugin.TokenCacheKey{
key := Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
}
tokenCache := credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
tokenCache := TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, tokenCache); err != nil {
t.Errorf("err wants nil but %+v", err)
}

View File

@@ -5,7 +5,6 @@ package di
import (
"github.com/google/wire"
"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"
@@ -13,19 +12,21 @@ import (
"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"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() adaptors.Cmd {
func NewCmd() cmd.Interface {
wire.Build(
// use-cases
auth.Set,
auth.ExtraSet,
login.Set,
wire.Value(auth.DefaultLocalServerReadyFunc),
standalone.Set,
credentialPluginUseCase.Set,
// adaptors
cmd.Set,
env.Set,
kubeconfig.Set,
@@ -38,14 +39,10 @@ func NewCmd() adaptors.Cmd {
}
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(
adaptors.Logger,
usecases.LoginShowLocalServerURL,
adaptors.CredentialPluginInteraction,
) adaptors.Cmd {
func NewCmdForHeadless(logger.Interface, auth.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
wire.Build(
auth.Set,
login.Set,
standalone.Set,
credentialPluginUseCase.Set,
cmd.Set,
env.Set,

View File

@@ -6,7 +6,6 @@
package di
import (
"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"
@@ -14,36 +13,37 @@ import (
"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"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
// Injectors from di.go:
func NewCmd() adaptors.Cmd {
adaptorsLogger := logger.New()
func NewCmd() cmd.Interface {
loggerInterface := logger.New()
factory := &oidc.Factory{
Logger: adaptorsLogger,
Logger: loggerInterface,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
showLocalServerURL := &auth.ShowLocalServerURL{
Logger: adaptorsLogger,
}
localServerReadyFunc := _wireLocalServerReadyFuncValue
authentication := &auth.Authentication{
OIDC: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: showLocalServerURL,
OIDCFactory: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
standaloneStandalone := &standalone.Standalone{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
interaction := &credentialplugin.Interaction{}
@@ -51,46 +51,62 @@ func NewCmd() adaptors.Cmd {
Authentication: authentication,
TokenCacheRepository: repository,
Interaction: interaction,
Logger: adaptorsLogger,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
GetToken: getToken,
Logger: loggerInterface,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
GetToken: getToken,
Logger: adaptorsLogger,
Root: root,
GetToken: cmdGetToken,
Logger: loggerInterface,
}
return cmdCmd
}
func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL, credentialPluginInteraction adaptors.CredentialPluginInteraction) adaptors.Cmd {
var (
_wireLocalServerReadyFuncValue = auth.DefaultLocalServerReadyFunc
)
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc auth.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
factory := &oidc.Factory{
Logger: adaptorsLogger,
Logger: loggerInterface,
}
decoder := &oidc.Decoder{}
envEnv := &env.Env{}
authentication := &auth.Authentication{
OIDC: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: adaptorsLogger,
ShowLocalServerURL: loginShowLocalServerURL,
OIDCFactory: factory,
OIDCDecoder: decoder,
Env: envEnv,
Logger: loggerInterface,
LocalServerReadyFunc: localServerReadyFunc,
}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
loginLogin := &login.Login{
standaloneStandalone := &standalone.Standalone{
Authentication: authentication,
Kubeconfig: kubeconfigKubeconfig,
Logger: adaptorsLogger,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repository := &tokencache.Repository{}
getToken := &credentialplugin2.GetToken{
Authentication: authentication,
TokenCacheRepository: repository,
Interaction: credentialPluginInteraction,
Logger: adaptorsLogger,
Interaction: credentialpluginInterface,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
GetToken: getToken,
Logger: loggerInterface,
}
cmdCmd := &cmd.Cmd{
Login: loginLogin,
GetToken: getToken,
Logger: adaptorsLogger,
Root: root,
GetToken: cmdGetToken,
Logger: loggerInterface,
}
return cmdCmd
}

View File

@@ -1,22 +0,0 @@
// Package credentialplugin provides models for the credential plugin.
package credentialplugin
import "time"
// 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"`
}
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}

View File

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

View File

@@ -5,22 +5,51 @@ import (
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors"
"github.com/int128/kubelogin/pkg/usecases"
"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"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_auth/mock_auth.go github.com/int128/kubelogin/pkg/usecases/auth Interface
// Set provides the use-case of Authentication.
var Set = wire.NewSet(
wire.Struct(new(Authentication), "*"),
wire.Bind(new(usecases.Authentication), new(*Authentication)),
wire.Bind(new(Interface), new(*Authentication)),
)
// ExtraSet is a set of interaction components for e2e testing.
var ExtraSet = wire.NewSet(
wire.Struct(new(ShowLocalServerURL), "*"),
wire.Bind(new(usecases.LoginShowLocalServerURL), new(*ShowLocalServerURL)),
)
// LocalServerReadyFunc provides an extension point for e2e tests.
type LocalServerReadyFunc func(url string)
// DefaultLocalServerReadyFunc is the default noop function.
var DefaultLocalServerReadyFunc = LocalServerReadyFunc(nil)
type Interface interface {
Do(ctx context.Context, in Input) (*Output, error)
}
// Input represents an input DTO of the Authentication use-case.
type Input struct {
OIDCConfig kubeconfig.OIDCConfig
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
}
// Output represents an output DTO of the Authentication use-case.
type Output struct {
AlreadyHasValidIDToken bool
IDTokenExpiry time.Time
IDTokenClaims map[string]string
IDToken string
RefreshToken string
}
const passwordPrompt = "Password: "
@@ -38,16 +67,16 @@ const passwordPrompt = "Password: "
// If the Password is not set, it asks a password by the prompt.
//
type Authentication struct {
OIDC adaptors.OIDC
OIDCDecoder adaptors.OIDCDecoder
Env adaptors.Env
Logger adaptors.Logger
ShowLocalServerURL usecases.LoginShowLocalServerURL
OIDCFactory oidc.FactoryInterface
OIDCDecoder oidc.DecoderInterface
Env env.Interface
Logger logger.Interface
LocalServerReadyFunc LocalServerReadyFunc // only for e2e tests
}
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
if in.OIDCConfig.IDToken != "" {
u.Logger.Debugf(1, "checking expiration of the existing token")
u.Logger.V(1).Infof("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.
@@ -56,8 +85,8 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
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{
u.Logger.V(1).Infof("you already have a valid token until %s", token.IDTokenExpiry)
return &Output{
AlreadyHasValidIDToken: true,
IDToken: in.OIDCConfig.IDToken,
RefreshToken: in.OIDCConfig.RefreshToken,
@@ -65,80 +94,102 @@ func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (
IDTokenClaims: token.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "you have an expired token at %s", token.IDTokenExpiry)
u.Logger.V(1).Infof("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{
u.Logger.V(1).Infof("initializing an OIDCFactory client")
client, err := u.OIDCFactory.New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: in.CACertFilename,
SkipTLSVerify: in.SkipTLSVerify,
})
if err != nil {
return nil, xerrors.Errorf("could not create an OIDC client: %w", err)
return nil, xerrors.Errorf("could not create an OIDCFactory client: %w", err)
}
if in.OIDCConfig.RefreshToken != "" {
u.Logger.Debugf(1, "refreshing the token")
out, err := client.Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: in.OIDCConfig.RefreshToken,
})
u.Logger.V(1).Infof("refreshing the token")
out, err := client.Refresh(ctx, in.OIDCConfig.RefreshToken)
if err == nil {
return &usecases.AuthenticationOut{
return &Output{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
}
u.Logger.Debugf(1, "could not refresh the token: %s", err)
u.Logger.V(1).Infof("could not refresh the token: %s", err)
}
if in.Username == "" {
u.Logger.Debugf(1, "performing the authentication code flow")
out, err := client.AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: in.ListenPort,
SkipOpenBrowser: in.SkipOpenBrowser,
ShowLocalServerURL: u.ShowLocalServerURL,
})
if err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &usecases.AuthenticationOut{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
}, nil
return u.doAuthCodeFlow(ctx, in, client)
}
return u.doPasswordCredentialsFlow(ctx, in, client)
}
u.Logger.Debugf(1, "performing the resource owner password credentials flow")
func (u *Authentication) doAuthCodeFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the authentication code flow")
readyChan := make(chan string, 1)
var out Output
var eg errgroup.Group
eg.Go(func() error {
select {
case url, ok := <-readyChan:
if !ok {
return nil
}
u.Logger.Printf("Open %s for authentication", url)
if u.LocalServerReadyFunc != nil {
u.LocalServerReadyFunc(url)
}
if in.SkipOpenBrowser {
return nil
}
if err := u.Env.OpenBrowser(url); err != nil {
u.Logger.V(1).Infof("could not open the browser: %s", err)
}
return nil
case <-ctx.Done():
return nil
}
})
eg.Go(func() error {
defer close(readyChan)
tokenSet, err := client.AuthenticateByCode(ctx, in.ListenPort, readyChan)
if err != nil {
return xerrors.Errorf("error while the authorization code flow: %w", err)
}
out = Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}
return nil
})
if err := eg.Wait(); err != nil {
return nil, xerrors.Errorf("error while the authorization code flow: %w", err)
}
return &out, nil
}
func (u *Authentication) doPasswordCredentialsFlow(ctx context.Context, in Input, client oidc.Interface) (*Output, error) {
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
if in.Password == "" {
var err error
in.Password, err = u.Env.ReadPassword(passwordPrompt)
if err != nil {
return nil, xerrors.Errorf("could not read a password: %w", err)
}
}
out, err := client.AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Username: in.Username,
Password: in.Password,
})
tokenSet, err := client.AuthenticateByPassword(ctx, in.Username, in.Password)
if err != nil {
return nil, xerrors.Errorf("error while the resource owner password credentials flow: %w", err)
}
return &usecases.AuthenticationOut{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
IDTokenExpiry: out.IDTokenExpiry,
IDTokenClaims: out.IDTokenClaims,
return &Output{
IDToken: tokenSet.IDToken,
RefreshToken: tokenSet.RefreshToken,
IDTokenExpiry: tokenSet.IDTokenExpiry,
IDTokenClaims: tokenSet.IDTokenClaims,
}, nil
}
// ShowLocalServerURL just shows the URL of local server to console.
type ShowLocalServerURL struct {
Logger adaptors.Logger
}
func (s *ShowLocalServerURL) ShowLocalServerURL(url string) {
s.Logger.Printf("Open %s for authentication", url)
}

View File

@@ -7,10 +7,11 @@ import (
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
"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"
"github.com/int128/kubelogin/pkg/adaptors/env/mock_env"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/oidc"
"github.com/int128/kubelogin/pkg/adaptors/oidc/mock_oidc"
"golang.org/x/xerrors"
)
@@ -23,7 +24,7 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
ListenPort: []int{10000},
SkipOpenBrowser: true,
CACertFilename: "/path/to/cert",
@@ -33,35 +34,82 @@ func TestAuthentication_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: []int{10000},
SkipOpenBrowser: true,
}).
Return(&adaptors.OIDCAuthenticateOut{
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}
if diff := deep.Equal(want, out); diff != nil {
t.Error(diff)
}
})
t.Run("AuthorizationCodeFlow/OpenBrowser", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := Input{
ListenPort: []int{10000},
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
Do(func(_ context.Context, _ []int, readyChan chan<- string) {
readyChan <- "LOCAL_SERVER_URL"
}).
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{Config: in.OIDCConfig}).
Return(mockOIDCClient, nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().
OpenBrowser("LOCAL_SERVER_URL")
u := Authentication{
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
Env: mockEnv,
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
@@ -76,7 +124,7 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
Username: "USER",
Password: "PASS",
CACertFilename: "/path/to/cert",
@@ -86,35 +134,32 @@ func TestAuthentication_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Username: "USER",
Password: "PASS",
}).
Return(&adaptors.OIDCAuthenticateOut{
AuthenticateByPassword(ctx, "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDCFactory: mockOIDCFactory,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
@@ -129,43 +174,40 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
Username: "USER",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
AuthenticateByPassword(ctx, adaptors.OIDCAuthenticateByPasswordIn{
Username: "USER",
Password: "PASS",
}).
Return(&adaptors.OIDCAuthenticateOut{
AuthenticateByPassword(ctx, "USER", "PASS").
Return(&oidc.TokenSet{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
u := Authentication{
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDCFactory: mockOIDCFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
want := &Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
@@ -180,25 +222,25 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
Username: "USER",
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mock_adaptors.NewMockOIDCClient(ctrl), nil)
mockEnv := mock_adaptors.NewMockEnv(ctrl)
Return(mock_oidc.NewMockInterface(ctrl), nil)
mockEnv := mock_env.NewMockInterface(ctrl)
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("", xerrors.New("error"))
u := Authentication{
OIDC: mockOIDC,
Env: mockEnv,
Logger: mock_adaptors.NewLogger(t, ctrl),
OIDCFactory: mockOIDCFactory,
Env: mockEnv,
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err == nil {
@@ -213,30 +255,30 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
IDToken: "VALID_ID_TOKEN",
},
}
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("VALID_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
Return(&oidc.DecodedIDToken{
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Authentication{
OIDC: mock_adaptors.NewMockOIDC(ctrl),
OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl),
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
want := &Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
@@ -251,7 +293,7 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
@@ -259,40 +301,38 @@ func TestAuthentication_Do(t *testing.T) {
RefreshToken: "VALID_REFRESH_TOKEN",
},
}
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
Return(&oidc.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "VALID_REFRESH_TOKEN",
}).
Return(&adaptors.OIDCAuthenticateOut{
Refresh(ctx, "VALID_REFRESH_TOKEN").
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
OIDCFactory: mockOIDCFactory,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
@@ -307,7 +347,7 @@ func TestAuthentication_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.AuthenticationIn{
in := Input{
ListenPort: []int{10000},
OIDCConfig: kubeconfig.OIDCConfig{
ClientID: "YOUR_CLIENT_ID",
@@ -316,45 +356,41 @@ func TestAuthentication_Do(t *testing.T) {
RefreshToken: "EXPIRED_REFRESH_TOKEN",
},
}
mockOIDCDecoder := mock_adaptors.NewMockOIDCDecoder(ctrl)
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
mockOIDCDecoder.EXPECT().
DecodeIDToken("EXPIRED_ID_TOKEN").
Return(&adaptors.DecodedIDToken{
Return(&oidc.DecodedIDToken{
IDTokenExpiry: pastTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
mockOIDCClient := mock_oidc.NewMockInterface(ctrl)
mockOIDCClient.EXPECT().
Refresh(ctx, adaptors.OIDCRefreshIn{
RefreshToken: "EXPIRED_REFRESH_TOKEN",
}).
Refresh(ctx, "EXPIRED_REFRESH_TOKEN").
Return(nil, xerrors.New("token has expired"))
mockOIDCClient.EXPECT().
AuthenticateByCode(ctx, adaptors.OIDCAuthenticateByCodeIn{
LocalServerPort: []int{10000},
}).
Return(&adaptors.OIDCAuthenticateOut{
AuthenticateByCode(ctx, []int{10000}, gomock.Any()).
Return(&oidc.TokenSet{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
mockOIDC.EXPECT().
New(ctx, adaptors.OIDCClientConfig{
mockOIDCFactory := mock_oidc.NewMockFactoryInterface(ctrl)
mockOIDCFactory.EXPECT().
New(ctx, oidc.ClientConfig{
Config: in.OIDCConfig,
}).
Return(mockOIDCClient, nil)
u := Authentication{
OIDC: mockOIDC,
OIDCFactory: mockOIDCFactory,
OIDCDecoder: mockOIDCDecoder,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
out, err := u.Do(ctx, in)
if err != nil {
t.Errorf("Do returned error: %+v", err)
}
want := &usecases.AuthenticationOut{
want := &Output{
IDToken: "NEW_ID_TOKEN",
RefreshToken: "NEW_REFRESH_TOKEN",
IDTokenExpiry: futureTime,

View File

@@ -0,0 +1,48 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/usecases/auth (interfaces: Interface)
// Package mock_auth is a generated GoMock package.
package mock_auth
import (
context "context"
gomock "github.com/golang/mock/gomock"
auth "github.com/int128/kubelogin/pkg/usecases/auth"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 auth.Input) (*auth.Output, error) {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*auth.Output)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -7,36 +7,58 @@ import (
"context"
"github.com/google/wire"
"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"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_credentialplugin/mock_credentialplugin.go github.com/int128/kubelogin/pkg/usecases/credentialplugin Interface
var Set = wire.NewSet(
wire.Struct(new(GetToken), "*"),
wire.Bind(new(usecases.GetToken), new(*GetToken)),
wire.Bind(new(Interface), new(*GetToken)),
)
type GetToken struct {
Authentication usecases.Authentication
TokenCacheRepository adaptors.TokenCacheRepository
Interaction adaptors.CredentialPluginInteraction
Logger adaptors.Logger
type Interface interface {
Do(ctx context.Context, in Input) error
}
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")
// Input represents an input DTO of the GetToken use-case.
type Input 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
TokenCacheDir string
}
u.Logger.Debugf(1, "finding a token from cache directory %s", in.TokenCacheDir)
cacheKey := credentialplugin.TokenCacheKey{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
type GetToken struct {
Authentication auth.Interface
TokenCacheRepository tokencache.Interface
Interaction credentialplugin.Interface
Logger logger.Interface
}
func (u *GetToken) Do(ctx context.Context, in Input) error {
u.Logger.V(1).Infof("WARNING: log may contain your secrets such as token or password")
u.Logger.V(1).Infof("finding a token from cache directory %s", in.TokenCacheDir)
cacheKey := tokencache.Key{IssuerURL: in.IssuerURL, ClientID: in.ClientID}
cache, err := u.TokenCacheRepository.FindByKey(in.TokenCacheDir, cacheKey)
if err != nil {
u.Logger.Debugf(1, "could not find a token cache: %s", err)
cache = &credentialplugin.TokenCache{}
u.Logger.V(1).Infof("could not find a token cache: %s", err)
cache = &tokencache.TokenCache{}
}
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
out, err := u.Authentication.Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: in.IssuerURL,
ClientID: in.ClientID,
@@ -56,11 +78,11 @@ 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, "the ID token has the claim: %s=%v", k, v)
u.Logger.V(1).Infof("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)
cache := credentialplugin.TokenCache{
cache := tokencache.TokenCache{
IDToken: out.IDToken,
RefreshToken: out.RefreshToken,
}
@@ -69,7 +91,7 @@ func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
}
}
u.Logger.Debugf(1, "writing the token to client-go")
u.Logger.V(1).Infof("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 the token to client-go: %w", err)
}

View File

@@ -6,11 +6,14 @@ import (
"time"
"github.com/golang/mock/gomock"
"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"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/adaptors/tokencache/mock_tokencache"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"golang.org/x/xerrors"
)
@@ -22,7 +25,7 @@ func TestGetToken_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.GetTokenIn{
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
@@ -34,9 +37,9 @@ func TestGetToken_Do(t *testing.T) {
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
@@ -49,30 +52,30 @@ func TestGetToken_Do(t *testing.T) {
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(&usecases.AuthenticationOut{
Return(&auth.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
Return(nil, xerrors.New("file not found"))
tokenCacheRepository.EXPECT().
Save("/path/to/token-cache",
credentialplugin.TokenCacheKey{
tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
},
credentialplugin.TokenCache{
tokencache.TokenCache{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
})
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(credentialplugin.Output{
Token: "YOUR_ID_TOKEN",
@@ -82,7 +85,7 @@ func TestGetToken_Do(t *testing.T) {
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
Interaction: credentialPluginInteraction,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -93,15 +96,15 @@ func TestGetToken_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.GetTokenIn{
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
@@ -109,22 +112,22 @@ func TestGetToken_Do(t *testing.T) {
IDToken: "VALID_ID_TOKEN",
},
}).
Return(&usecases.AuthenticationOut{
Return(&auth.Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
Return(&credentialplugin.TokenCache{
Return(&tokencache.TokenCache{
IDToken: "VALID_ID_TOKEN",
}, nil)
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
credentialPluginInteraction.EXPECT().
Write(credentialplugin.Output{
Token: "VALID_ID_TOKEN",
@@ -134,7 +137,7 @@ func TestGetToken_Do(t *testing.T) {
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
Interaction: credentialPluginInteraction,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -145,15 +148,15 @@ func TestGetToken_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.GetTokenIn{
in := Input{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
TokenCacheDir: "/path/to/token-cache",
}
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
Do(ctx, auth.Input{
OIDCConfig: kubeconfig.OIDCConfig{
IDPIssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
@@ -161,9 +164,9 @@ func TestGetToken_Do(t *testing.T) {
},
}).
Return(nil, xerrors.New("authentication error"))
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
tokenCacheRepository.EXPECT().
FindByKey("/path/to/token-cache", credentialplugin.TokenCacheKey{
FindByKey("/path/to/token-cache", tokencache.Key{
IssuerURL: "https://accounts.google.com",
ClientID: "YOUR_CLIENT_ID",
}).
@@ -171,8 +174,8 @@ func TestGetToken_Do(t *testing.T) {
u := GetToken{
Authentication: mockAuthentication,
TokenCacheRepository: tokenCacheRepository,
Interaction: mock_adaptors.NewMockCredentialPluginInteraction(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
Interaction: mock_credentialplugin.NewMockInterface(ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")

View File

@@ -0,0 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/usecases/credentialplugin (interfaces: Interface)
// Package mock_credentialplugin is a generated GoMock package.
package mock_credentialplugin
import (
context "context"
gomock "github.com/golang/mock/gomock"
credentialplugin "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 credentialplugin.Input) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -1,76 +0,0 @@
package usecases
import (
"context"
"time"
"github.com/int128/kubelogin/pkg/models/kubeconfig"
)
//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
}
// LoginIn represents an input DTO of the Login use-case.
type LoginIn struct {
KubeconfigFilename string // Default to the environment variable or global config as kubectl
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
KubeconfigUser kubeconfig.UserName // Default to the user of the context
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
}
// LoginShowLocalServerURL provides an interface to notify the URL of local server.
// It is needed for the end-to-end tests.
type LoginShowLocalServerURL interface {
ShowLocalServerURL(url string)
}
type GetToken interface {
Do(ctx context.Context, in GetTokenIn) error
}
// 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
TokenCacheDir string
}
type Authentication interface {
Do(ctx context.Context, in AuthenticationIn) (*AuthenticationOut, error)
}
// AuthenticationIn represents an input DTO of the Authentication use-case.
type AuthenticationIn struct {
OIDCConfig kubeconfig.OIDCConfig
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
}
// AuthenticationIn represents an output DTO of the Authentication use-case.
type AuthenticationOut struct {
AlreadyHasValidIDToken bool
IDTokenExpiry time.Time
IDTokenClaims map[string]string
IDToken string
RefreshToken string
}

View File

@@ -1,76 +0,0 @@
package login
import (
"context"
"github.com/google/wire"
"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.Bind(new(usecases.Login), new(*Login)),
)
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials CONTEXT_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
// Login provides the use case of explicit login.
//
// If the current auth provider is not oidc, show the error.
// If the kubeconfig has a valid token, do nothing.
// Otherwise, update the kubeconfig.
//
type Login struct {
Authentication usecases.Authentication
Kubeconfig adaptors.Kubeconfig
Logger adaptors.Logger
}
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
authProvider, err := u.Kubeconfig.GetCurrentAuthProvider(in.KubeconfigFilename, in.KubeconfigContext, in.KubeconfigUser)
if err != nil {
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)
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, "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)
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
}

View File

@@ -1,118 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/usecases (interfaces: Login,GetToken,Authentication)
// Package mock_usecases is a generated GoMock package.
package mock_usecases
import (
context "context"
gomock "github.com/golang/mock/gomock"
usecases "github.com/int128/kubelogin/pkg/usecases"
reflect "reflect"
)
// MockLogin is a mock of Login interface
type MockLogin struct {
ctrl *gomock.Controller
recorder *MockLoginMockRecorder
}
// MockLoginMockRecorder is the mock recorder for MockLogin
type MockLoginMockRecorder struct {
mock *MockLogin
}
// NewMockLogin creates a new mock instance
func NewMockLogin(ctrl *gomock.Controller) *MockLogin {
mock := &MockLogin{ctrl: ctrl}
mock.recorder = &MockLoginMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLogin) EXPECT() *MockLoginMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockLogin) Do(arg0 context.Context, arg1 usecases.LoginIn) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Do indicates an expected call of Do
func (mr *MockLoginMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockLogin)(nil).Do), arg0, arg1)
}
// MockGetToken is a mock of GetToken interface
type MockGetToken struct {
ctrl *gomock.Controller
recorder *MockGetTokenMockRecorder
}
// MockGetTokenMockRecorder is the mock recorder for MockGetToken
type MockGetTokenMockRecorder struct {
mock *MockGetToken
}
// NewMockGetToken creates a new mock instance
func NewMockGetToken(ctrl *gomock.Controller) *MockGetToken {
mock := &MockGetToken{ctrl: ctrl}
mock.recorder = &MockGetTokenMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockGetToken) EXPECT() *MockGetTokenMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockGetToken) Do(arg0 context.Context, arg1 usecases.GetTokenIn) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Do indicates an expected call of Do
func (mr *MockGetTokenMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockGetToken)(nil).Do), arg0, arg1)
}
// MockAuthentication is a mock of Authentication interface
type MockAuthentication struct {
ctrl *gomock.Controller
recorder *MockAuthenticationMockRecorder
}
// MockAuthenticationMockRecorder is the mock recorder for MockAuthentication
type MockAuthenticationMockRecorder struct {
mock *MockAuthentication
}
// NewMockAuthentication creates a new mock instance
func NewMockAuthentication(ctrl *gomock.Controller) *MockAuthentication {
mock := &MockAuthentication{ctrl: ctrl}
mock.recorder = &MockAuthenticationMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAuthentication) EXPECT() *MockAuthenticationMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockAuthentication) Do(arg0 context.Context, arg1 usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(*usecases.AuthenticationOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do
func (mr *MockAuthenticationMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockAuthentication)(nil).Do), arg0, arg1)
}

View File

@@ -0,0 +1,47 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/usecases/standalone (interfaces: Interface)
// Package mock_standalone is a generated GoMock package.
package mock_standalone
import (
context "context"
gomock "github.com/golang/mock/gomock"
standalone "github.com/int128/kubelogin/pkg/usecases/standalone"
reflect "reflect"
)
// MockInterface is a mock of Interface interface
type MockInterface struct {
ctrl *gomock.Controller
recorder *MockInterfaceMockRecorder
}
// MockInterfaceMockRecorder is the mock recorder for MockInterface
type MockInterfaceMockRecorder struct {
mock *MockInterface
}
// NewMockInterface creates a new mock instance
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
mock := &MockInterface{ctrl: ctrl}
mock.recorder = &MockInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
return m.recorder
}
// Do mocks base method
func (m *MockInterface) Do(arg0 context.Context, arg1 standalone.Input) error {
ret := m.ctrl.Call(m, "Do", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Do indicates an expected call of Do
func (mr *MockInterfaceMockRecorder) Do(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockInterface)(nil).Do), arg0, arg1)
}

View File

@@ -0,0 +1,96 @@
package standalone
import (
"context"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_standalone/mock_standalone.go github.com/int128/kubelogin/pkg/usecases/standalone Interface
// Set provides the use-case.
var Set = wire.NewSet(
wire.Struct(new(Standalone), "*"),
wire.Bind(new(Interface), new(*Standalone)),
)
type Interface interface {
Do(ctx context.Context, in Input) error
}
// Input represents an input DTO of the use-case.
type Input struct {
KubeconfigFilename string // Default to the environment variable or global config as kubectl
KubeconfigContext kubeconfig.ContextName // Default to the current context but ignored if KubeconfigUser is set
KubeconfigUser kubeconfig.UserName // Default to the user of the context
SkipOpenBrowser bool
ListenPort []int
Username string // If set, perform the resource owner password credentials grant
Password string // If empty, read a password using Env.ReadPassword()
CACertFilename string // If set, use the CA cert
SkipTLSVerify bool
}
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
kubectl config set-credentials CONTEXT_NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
// Standalone provides the use case of explicit login.
//
// If the current auth provider is not oidc, show the error.
// If the kubeconfig has a valid token, do nothing.
// Otherwise, update the kubeconfig.
//
type Standalone struct {
Authentication auth.Interface
Kubeconfig kubeconfig.Interface
Logger logger.Interface
}
func (u *Standalone) Do(ctx context.Context, in Input) error {
u.Logger.V(1).Infof("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.Printf(oidcConfigErrorMessage)
return xerrors.Errorf("could not find the current authentication provider: %w", err)
}
u.Logger.V(1).Infof("using the authentication provider of the user %s", authProvider.UserName)
u.Logger.V(1).Infof("a token will be written to %s", authProvider.LocationOfOrigin)
out, err := u.Authentication.Do(ctx, auth.Input{
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.V(1).Infof("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)
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.V(1).Infof("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
}

View File

@@ -1,4 +1,4 @@
package login
package standalone
import (
"context"
@@ -6,14 +6,15 @@ import (
"time"
"github.com/golang/mock/gomock"
"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"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig/mock_kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
"github.com/int128/kubelogin/pkg/usecases/auth"
"github.com/int128/kubelogin/pkg/usecases/auth/mock_auth"
"golang.org/x/xerrors"
)
func TestLogin_Do(t *testing.T) {
func TestStandalone_Do(t *testing.T) {
dummyTokenClaims := map[string]string{"sub": "YOUR_SUBJECT"}
futureTime := time.Now().Add(time.Hour) //TODO: inject time service
@@ -21,7 +22,7 @@ func TestLogin_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginIn{
in := Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "theContext",
KubeconfigUser: "theUser",
@@ -41,7 +42,7 @@ func TestLogin_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("/path/to/kubeconfig", kubeconfig.ContextName("theContext"), kubeconfig.UserName("theUser")).
Return(currentAuthProvider, nil)
@@ -57,9 +58,9 @@ func TestLogin_Do(t *testing.T) {
RefreshToken: "YOUR_REFRESH_TOKEN",
},
})
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{
Do(ctx, auth.Input{
OIDCConfig: currentAuthProvider.OIDCConfig,
ListenPort: []int{10000},
SkipOpenBrowser: true,
@@ -68,16 +69,16 @@ func TestLogin_Do(t *testing.T) {
CACertFilename: "/path/to/cert",
SkipTLSVerify: true,
}).
Return(&usecases.AuthenticationOut{
Return(&auth.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Login{
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -88,7 +89,7 @@ func TestLogin_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginIn{}
in := Input{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
@@ -98,23 +99,23 @@ func TestLogin_Do(t *testing.T) {
IDToken: "VALID_ID_TOKEN",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&usecases.AuthenticationOut{
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&auth.Output{
AlreadyHasValidIDToken: true,
IDToken: "VALID_ID_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Login{
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err != nil {
t.Errorf("Do returned error: %+v", err)
@@ -125,16 +126,16 @@ func TestLogin_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginIn{}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
in := Input{}
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(nil, xerrors.New("no oidc config"))
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
u := Login{
mockAuthentication := mock_auth.NewMockInterface(ctrl)
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
@@ -145,7 +146,7 @@ func TestLogin_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginIn{}
in := Input{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
@@ -155,18 +156,18 @@ func TestLogin_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(nil, xerrors.New("authentication error"))
u := Login{
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")
@@ -177,7 +178,7 @@ func TestLogin_Do(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
in := usecases.LoginIn{}
in := Input{}
currentAuthProvider := &kubeconfig.AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "google",
@@ -187,7 +188,7 @@ func TestLogin_Do(t *testing.T) {
ClientSecret: "YOUR_CLIENT_SECRET",
},
}
mockKubeconfig := mock_adaptors.NewMockKubeconfig(ctrl)
mockKubeconfig := mock_kubeconfig.NewMockInterface(ctrl)
mockKubeconfig.EXPECT().
GetCurrentAuthProvider("", kubeconfig.ContextName(""), kubeconfig.UserName("")).
Return(currentAuthProvider, nil)
@@ -204,19 +205,19 @@ func TestLogin_Do(t *testing.T) {
},
}).
Return(xerrors.New("I/O error"))
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
mockAuthentication := mock_auth.NewMockInterface(ctrl)
mockAuthentication.EXPECT().
Do(ctx, usecases.AuthenticationIn{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&usecases.AuthenticationOut{
Do(ctx, auth.Input{OIDCConfig: currentAuthProvider.OIDCConfig}).
Return(&auth.Output{
IDToken: "YOUR_ID_TOKEN",
RefreshToken: "YOUR_REFRESH_TOKEN",
IDTokenExpiry: futureTime,
IDTokenClaims: dummyTokenClaims,
}, nil)
u := Login{
u := Standalone{
Authentication: mockAuthentication,
Kubeconfig: mockKubeconfig,
Logger: mock_adaptors.NewLogger(t, ctrl),
Logger: mock_logger.New(t),
}
if err := u.Do(ctx, in); err == nil {
t.Errorf("err wants non-nil but nil")

19
snapcraft.yaml Normal file
View File

@@ -0,0 +1,19 @@
name: kubelogin
version: git
summary: Log in to the OpenID Connect provider
description: |
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
confinement: strict
base: core18
parts:
kubelogin:
plugin: nil
source: .
source-type: git
build-snaps: [go]
override-build: |
make CIRCLE_TAG=$SNAPCRAFT_PROJECT_VERSION
cp -av kubelogin $SNAPCRAFT_PART_INSTALL/bin
apps:
kubelogin:
command: bin/kubelogin