mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-28 16:00:19 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c7c1e703 | ||
|
|
6b1e11f071 | ||
|
|
554daf7655 | ||
|
|
d67d16b333 | ||
|
|
3d0973054b | ||
|
|
bf02210f2a | ||
|
|
53e8284b63 | ||
|
|
d9b8d99fae | ||
|
|
3e30346c9b | ||
|
|
1e80481145 | ||
|
|
9242b1917b | ||
|
|
306bf09485 | ||
|
|
4ad77cd5f8 | ||
|
|
c8967faf6b | ||
|
|
315d6151d7 | ||
|
|
1ff03fdfb3 | ||
|
|
5e0fc7f399 | ||
|
|
9423a65f46 | ||
|
|
45417a18fd | ||
|
|
760416fd04 | ||
|
|
0a4ebb26c2 | ||
|
|
de9f7a2a01 | ||
|
|
0006cdda2d | ||
|
|
c89a8a1823 |
47
DESIGN.md
47
DESIGN.md
@@ -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.
|
||||
|
||||
|
||||
12
Makefile
12
Makefile
@@ -3,13 +3,11 @@ TARGET_PLUGIN := kubectl-oidc_login
|
||||
CIRCLE_TAG ?= HEAD
|
||||
LDFLAGS := -X main.version=$(CIRCLE_TAG)
|
||||
|
||||
.PHONY: check run diagram release clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
golangci-lint run
|
||||
$(MAKE) -C e2e_test/keys/testdata
|
||||
go test -v -race -cover -coverprofile=coverage.out ./...
|
||||
|
||||
$(TARGET): $(wildcard *.go)
|
||||
@@ -18,25 +16,23 @@ $(TARGET): $(wildcard *.go)
|
||||
$(TARGET_PLUGIN): $(TARGET)
|
||||
ln -sf $(TARGET) $@
|
||||
|
||||
.PHONY: run
|
||||
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/
|
||||
mkdir -p dist/plugins
|
||||
cp dist/gh/oidc-login.yaml dist/plugins/oidc-login.yaml
|
||||
|
||||
.PHONY: release
|
||||
release: dist
|
||||
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
|
||||
ghcp commit -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
|
||||
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(CIRCLE_TAG)" -m "Bump oidc-login to $(CIRCLE_TAG)" -C dist/ plugins/oidc-login.yaml
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm $(TARGET)
|
||||
-rm $(TARGET_PLUGIN)
|
||||
|
||||
247
README.md
247
README.md
@@ -2,47 +2,40 @@
|
||||
|
||||
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`.
|
||||
|
||||
Kubelogin integrates browser based authentication with kubectl.
|
||||
You do not need to manually set an ID token and refresh token to the kubeconfig.
|
||||
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 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.0/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)
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
You can run kubelogin as the following methods:
|
||||
|
||||
- Run as a credential plugin
|
||||
- Run as a standalone command
|
||||
|
||||
|
||||
### Run as a credential plugin
|
||||
|
||||
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:
|
||||
@@ -59,6 +52,8 @@ users:
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
### Run
|
||||
|
||||
Run kubectl.
|
||||
|
||||
```sh
|
||||
@@ -86,77 +81,20 @@ If the cached ID token is valid, kubelogin just returns it.
|
||||
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
|
||||
If the refresh token has expired, kubelogin will perform reauthentication.
|
||||
|
||||
You can log out by removing the token cache file (default `~/.kube/oidc-login.token-cache`).
|
||||
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
|
||||
|
||||
### Run as a standalone command
|
||||
|
||||
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 to the cluster.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
|
||||
### Run as a credential plugin
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
@@ -177,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 string Path to a file for caching the token (default "~/.kube/oidc-login.token-cache")
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
-h, --help help for get-token
|
||||
|
||||
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`.
|
||||
|
||||
@@ -191,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
|
||||
```
|
||||
|
||||
|
||||
### Run as a standalone command
|
||||
|
||||
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`.
|
||||
@@ -315,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.
|
||||
@@ -332,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
|
||||
@@ -344,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.
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"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)),
|
||||
)
|
||||
|
||||
const examples = ` # Login to the provider using the authorization code flow.
|
||||
%[1]s
|
||||
|
||||
# Login to the provider using the resource owner password credentials flow.
|
||||
%[1]s --username USERNAME --password PASSWORD
|
||||
|
||||
# Run as a credential plugin.
|
||||
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
|
||||
|
||||
var defaultListenPort = []int{8000, 18000}
|
||||
var defaultTokenCache = homedir.HomeDir() + "/.kube/oidc-login.token-cache"
|
||||
|
||||
// Cmd provides interaction with command line interface (CLI).
|
||||
type Cmd struct {
|
||||
Login usecases.Login
|
||||
GetToken usecases.GetToken
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
// Run parses the command line arguments and executes the specified use-case.
|
||||
// It returns an exit code, that is 0 on success or 1 on error.
|
||||
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
executable := filepath.Base(args[0])
|
||||
|
||||
rootCmd := newRootCmd(ctx, executable, cmd)
|
||||
rootCmd.Version = version
|
||||
rootCmd.SilenceUsage = true
|
||||
rootCmd.SilenceErrors = true
|
||||
|
||||
getTokenCmd := newGetTokenCmd(ctx, cmd)
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
cmd.Logger.Printf("%s version %s", executable, version)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
rootCmd.SetArgs(args[1:])
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
cmd.Logger.Printf("error: %s", err)
|
||||
cmd.Logger.Debugf(1, "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
|
||||
TokenCacheFilename 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.TokenCacheFilename, "token-cache", defaultTokenCache, "Path to a file for caching the token")
|
||||
}
|
||||
|
||||
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,
|
||||
TokenCacheFilename: o.TokenCacheFilename,
|
||||
}
|
||||
if err := cmd.GetToken.Do(ctx, in); err != nil {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,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 {
|
||||
Read(filename string) (*credentialplugin.TokenCache, error)
|
||||
Write(filename string, tc 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)
|
||||
Verify(ctx context.Context, in OIDCVerifyIn) (*OIDCVerifyOut, 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
|
||||
}
|
||||
|
||||
// OIDCVerifyIn represents an input DTO of OIDCClient.Verify.
|
||||
type OIDCVerifyIn struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
// OIDCVerifyIn represents an output DTO of OIDCClient.Verify.
|
||||
type OIDCVerifyOut struct {
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
// OIDCRefreshIn represents an input DTO of OIDCClient.Refresh.
|
||||
type OIDCRefreshIn struct {
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
type 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
|
||||
@@ -1,14 +0,0 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Kubeconfig), "*"),
|
||||
wire.Bind(new(adaptors.Kubeconfig), new(*Kubeconfig)),
|
||||
)
|
||||
|
||||
type Kubeconfig struct{}
|
||||
@@ -1,56 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Logger.
|
||||
var Set = wire.NewSet(
|
||||
New,
|
||||
)
|
||||
|
||||
// New returns a Logger with the standard log.Logger for messages and debug.
|
||||
func New() adaptors.Logger {
|
||||
return &Logger{
|
||||
stdLogger: log.New(os.Stderr, "", 0),
|
||||
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds|log.Lshortfile),
|
||||
}
|
||||
}
|
||||
|
||||
func NewWith(s stdLogger, d debugLogger) *Logger {
|
||||
return &Logger{s, d, 0}
|
||||
}
|
||||
|
||||
type stdLogger 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.
|
||||
type Logger struct {
|
||||
stdLogger
|
||||
debugLogger
|
||||
level adaptors.LogLevel
|
||||
}
|
||||
|
||||
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
|
||||
if l.IsEnabled(level) {
|
||||
_ = l.debugLogger.Output(2, fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) SetLevel(level adaptors.LogLevel) {
|
||||
l.level = level
|
||||
}
|
||||
|
||||
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
|
||||
return level <= l.level
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/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)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package mock_adaptors
|
||||
|
||||
import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/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...)
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,TokenCacheRepository,CredentialPluginInteraction,OIDC,OIDCClient,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/adaptors"
|
||||
credentialplugin "github.com/int128/kubelogin/models/credentialplugin"
|
||||
kubeconfig "github.com/int128/kubelogin/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
|
||||
}
|
||||
|
||||
// Read mocks base method
|
||||
func (m *MockTokenCacheRepository) Read(arg0 string) (*credentialplugin.TokenCache, error) {
|
||||
ret := m.ctrl.Call(m, "Read", arg0)
|
||||
ret0, _ := ret[0].(*credentialplugin.TokenCache)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Read indicates an expected call of Read
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Read(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockTokenCacheRepository)(nil).Read), arg0)
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
func (m *MockTokenCacheRepository) Write(arg0 string, arg1 credentialplugin.TokenCache) error {
|
||||
ret := m.ctrl.Call(m, "Write", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Write indicates an expected call of Write
|
||||
func (mr *MockTokenCacheRepositoryMockRecorder) Write(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockTokenCacheRepository)(nil).Write), arg0, arg1)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Verify mocks base method
|
||||
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
|
||||
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
|
||||
ret0, _ := ret[0].(*adaptors.OIDCVerifyOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Verify indicates an expected call of Verify
|
||||
func (mr *MockOIDCClientMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDCClient)(nil).Verify), arg0, arg1)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelDumpHeaders = 2
|
||||
logLevelDumpBody = 3
|
||||
)
|
||||
|
||||
type Transport struct {
|
||||
Base http.RoundTripper
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if !t.IsDumpEnabled() {
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
|
||||
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
|
||||
resp, err := t.Base.RoundTrip(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
|
||||
return resp, err
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (t *Transport) IsDumpEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpHeaders)
|
||||
}
|
||||
|
||||
func (t *Transport) IsDumpBodyEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpBody)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
t.req = req
|
||||
return t.resp, nil
|
||||
}
|
||||
|
||||
func TestLoggingTransport_RoundTrip(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(gomock.Any()).
|
||||
Return(true).
|
||||
AnyTimes()
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
|
||||
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
|
||||
Host: example.com
|
||||
|
||||
dummy`)), req)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a response: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
transport := &Transport{
|
||||
Base: &mockTransport{resp: resp},
|
||||
Logger: logger,
|
||||
}
|
||||
gotResp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("RoundTrip error: %s", err)
|
||||
}
|
||||
if gotResp != resp {
|
||||
t.Errorf("resp wants %v but %v", resp, gotResp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
|
||||
Return(true)
|
||||
|
||||
transport := &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")
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
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/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/logging"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/tls"
|
||||
"github.com/int128/oauth2cli"
|
||||
"github.com/pkg/browser"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// In credential plugin mode, some browser launcher writes a message to stdout
|
||||
// and it may break the credential json for client-go.
|
||||
// This prevents the browser launcher from breaking the credential json.
|
||||
browser.Stdout = os.Stderr
|
||||
}
|
||||
|
||||
// Set provides an implementation and interface for OIDC.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Factory), "*"),
|
||||
wire.Bind(new(adaptors.OIDC), new(*Factory)),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Verify checks client ID and signature of the ID token.
|
||||
// This does not check the expiration and caller should check it.
|
||||
func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
verifier := c.provider.Verifier(&oidc.Config{
|
||||
ClientID: c.oauth2Config.ClientID,
|
||||
SkipExpiryCheck: true,
|
||||
})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
|
||||
}
|
||||
claims, err := dumpClaims(verifiedIDToken)
|
||||
if err != nil {
|
||||
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
|
||||
}
|
||||
return &adaptors.OIDCVerifyOut{
|
||||
IDTokenExpiry: verifiedIDToken.Expiry,
|
||||
IDTokenClaims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refresh sends a refresh token request and returns a token set.
|
||||
func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
ctx = c.wrapContext(ctx)
|
||||
currentToken := &oauth2.Token{
|
||||
Expiry: time.Now(),
|
||||
RefreshToken: in.RefreshToken,
|
||||
}
|
||||
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)
|
||||
claims := make(map[string]string)
|
||||
for k, v := range rawClaims {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
claims[k] = fmt.Sprintf("%f", v.(float64))
|
||||
default:
|
||||
claims[k] = fmt.Sprintf("%s", v)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return claims, xerrors.Errorf("error while decoding the ID token: %w", err)
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/int128/kubelogin/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
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package tokencache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Repository), "*"),
|
||||
wire.Bind(new(adaptors.TokenCacheRepository), new(*Repository)),
|
||||
)
|
||||
|
||||
type Repository struct{}
|
||||
|
||||
func (*Repository) Read(filename string) (*credentialplugin.TokenCache, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
var c credentialplugin.TokenCache
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode json file %s: %w", filename, err)
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (*Repository) Write(filename string, tc credentialplugin.TokenCache) error {
|
||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not create file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
e := json.NewEncoder(f)
|
||||
if err := e.Encode(&tc); err != nil {
|
||||
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
di/di.go
57
di/di.go
@@ -1,57 +0,0 @@
|
||||
//+build wireinject
|
||||
|
||||
// Package di provides dependency injection.
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
credentialPluginAdaptor "github.com/int128/kubelogin/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/adaptors/env"
|
||||
"github.com/int128/kubelogin/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
)
|
||||
|
||||
// NewCmd returns an instance of adaptors.Cmd.
|
||||
func NewCmd() adaptors.Cmd {
|
||||
wire.Build(
|
||||
auth.Set,
|
||||
auth.ExtraSet,
|
||||
login.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
credentialPluginAdaptor.Set,
|
||||
oidc.Set,
|
||||
logger.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
|
||||
func NewCmdForHeadless(
|
||||
adaptors.Logger,
|
||||
usecases.LoginShowLocalServerURL,
|
||||
adaptors.CredentialPluginInteraction,
|
||||
) adaptors.Cmd {
|
||||
wire.Build(
|
||||
auth.Set,
|
||||
login.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
oidc.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate wire
|
||||
//+build !wireinject
|
||||
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/adaptors/env"
|
||||
"github.com/int128/kubelogin/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
credentialplugin2 "github.com/int128/kubelogin/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
)
|
||||
|
||||
// Injectors from di.go:
|
||||
|
||||
func NewCmd() adaptors.Cmd {
|
||||
adaptorsLogger := logger.New()
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
envEnv := &env.Env{}
|
||||
showLocalServerURL := &auth.ShowLocalServerURL{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
authentication := &auth.Authentication{
|
||||
OIDC: factory,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
ShowLocalServerURL: showLocalServerURL,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
|
||||
loginLogin := &login.Login{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
interaction := &credentialplugin.Interaction{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: interaction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
|
||||
func NewCmdForHeadless(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL, credentialPluginInteraction adaptors.CredentialPluginInteraction) adaptors.Cmd {
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
envEnv := &env.Env{}
|
||||
authentication := &auth.Authentication{
|
||||
OIDC: factory,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
ShowLocalServerURL: loginShowLocalServerURL,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
|
||||
loginLogin := &login.Login{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: credentialPluginInteraction,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
GetToken: getToken,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
BIN
docs/authn.png
BIN
docs/authn.png
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
@@ -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;
|
||||
}
|
||||
141
docs/dex.md
Normal file
141
docs/dex.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Getting Started with dex and GitHub
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have a GitHub account.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed.
|
||||
|
||||
## 1. Setup GitHub OAuth
|
||||
|
||||
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
|
||||
|
||||
- Application name: (any)
|
||||
- Homepage URL: `https://dex.example.com`
|
||||
- Authorization callback URL: `https://dex.example.com/callback`
|
||||
|
||||
## 2. Setup dex
|
||||
|
||||
Configure the dex with the following config:
|
||||
|
||||
```yaml
|
||||
issuer: https://dex.example.com
|
||||
connectors:
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: YOUR_GITHUB_CLIENT_ID
|
||||
clientSecret: YOUR_GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://dex.example.com/callback
|
||||
staticClients:
|
||||
- id: kubernetes
|
||||
name: Kubernetes
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
secret: YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Now test authentication with the dex.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
--oidc-issuer-url=https://dex.example.com \
|
||||
--oidc-client-id=kubernetes \
|
||||
--oidc-client-secret=YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
|
||||
```
|
||||
--oidc-issuer-url=https://dex.example.com
|
||||
--oidc-client-id=kubernetes
|
||||
```
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://dex.example.com
|
||||
oidcClientID: kubernetes
|
||||
```
|
||||
|
||||
## 4. Create a role binding
|
||||
|
||||
Here assign the `cluster-admin` role to your subject.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: keycloak-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 5. Setup kubeconfig
|
||||
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.example.com
|
||||
name: example.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example.k8s.local
|
||||
user: dex
|
||||
name: dex@example.k8s.local
|
||||
current-context: dex@example.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: dex
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://dex.example.com
|
||||
- --oidc-client-id=kubernetes
|
||||
- --oidc-client-secret=YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 6. Run kubectl
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
|
||||
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
|
||||
```
|
||||
@@ -13,6 +13,23 @@ Open [Google APIs Console](https://console.developers.google.com/apis/credential
|
||||
|
||||
- Application Type: Other
|
||||
|
||||
Now test authentication with Google Identity Platform.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
--oidc-issuer-url=https://accounts.google.com \
|
||||
--oidc-client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
@@ -33,7 +50,7 @@ spec:
|
||||
|
||||
## 3. Setup Kubernetes cluster
|
||||
|
||||
Here assign the `cluster-admin` role to you.
|
||||
Here assign the `cluster-admin` role to your subject.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
@@ -46,7 +63,7 @@ roleRef:
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: https://accounts.google.com#1234567890
|
||||
name: YOUR_SUBJECT
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
@@ -56,6 +73,19 @@ You can create a custom role and assign it as well.
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.example.com
|
||||
name: example.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example.k8s.local
|
||||
user: google
|
||||
name: google@example.k8s.local
|
||||
current-context: google@example.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: google
|
||||
user:
|
||||
@@ -69,9 +99,11 @@ users:
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
|
||||
@@ -28,6 +28,24 @@ You can associate client roles by adding the following mapper:
|
||||
|
||||
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
|
||||
Now test authentication with the Keycloak.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login get-token -v1 \
|
||||
--oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
|
||||
--oidc-client-id=kubernetes \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You should get claims like:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
@@ -73,6 +91,19 @@ You can create a custom role and assign it as well.
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://api.example.com
|
||||
name: example.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: example.k8s.local
|
||||
user: keycloak
|
||||
name: keycloak@example.k8s.local
|
||||
current-context: keycloak@example.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
@@ -86,9 +117,11 @@ users:
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
|
||||
## 5. Run kubectl
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
|
||||
180
docs/standalone-mode.md
Normal file
180
docs/standalone-mode.md
Normal 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
|
||||
```
|
||||
@@ -1,42 +0,0 @@
|
||||
# Team on-boarding
|
||||
|
||||
## kops
|
||||
|
||||
Export the kubeconfig.
|
||||
|
||||
```sh
|
||||
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
|
||||
```
|
||||
|
||||
Remove the `admin` access from the kubeconfig.
|
||||
It should look as like:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: LS...
|
||||
server: https://api.hello.k8s.example.com
|
||||
name: hello.k8s.local
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: hello.k8s.local
|
||||
name: hello.k8s.local
|
||||
current-context: hello.k8s.local
|
||||
preferences: {}
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
args:
|
||||
- get-token
|
||||
- --oidc-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
@@ -2,19 +2,20 @@ package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/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/usecases/auth"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
@@ -26,6 +27,15 @@ import (
|
||||
//
|
||||
func TestCmd_Run_CredentialPlugin(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
cacheDir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a cache dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(cacheDir); err != nil {
|
||||
t.Errorf("could not clean up the cache dir: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -40,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) {
|
||||
@@ -52,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", "/dev/null",
|
||||
"--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)
|
||||
|
||||
3
e2e_test/keys/testdata/.gitignore
vendored
3
e2e_test/keys/testdata/.gitignore
vendored
@@ -1,4 +1 @@
|
||||
/CA
|
||||
*.key
|
||||
*.csr
|
||||
*.crt
|
||||
|
||||
9
e2e_test/keys/testdata/Makefile
vendored
9
e2e_test/keys/testdata/Makefile
vendored
@@ -1,13 +1,13 @@
|
||||
all: ca.key ca.crt server.key server.crt jws.key
|
||||
|
||||
.PHONY: clean
|
||||
|
||||
all: server.crt ca.crt jws.key
|
||||
|
||||
clean:
|
||||
rm -v ca.* server.*
|
||||
-rm -v ca.* server.* jws.*
|
||||
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.INTERMEDIATE: ca.csr
|
||||
ca.csr: openssl.cnf ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
@@ -26,6 +26,7 @@ ca.crt: ca.csr ca.key
|
||||
server.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.INTERMEDIATE: server.csr
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
|
||||
11
e2e_test/keys/testdata/ca.crt
vendored
Normal file
11
e2e_test/keys/testdata/ca.crt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnTCCAQYCCQCuPrhkr+BvGzANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhI
|
||||
ZWxsbyBDQTAeFw0xOTA4MTgwNjAwMDZaFw0xOTA5MTcwNjAwMDZaMBMxETAPBgNV
|
||||
BAMMCEhlbGxvIENBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDnSTDsRx4U
|
||||
JmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ4Pc51itOycWN1nclNVb489i9
|
||||
J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3Lu4mysxyOtYZNsdST8GBCP0m4
|
||||
2tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
|
||||
ACfgNePlOLnLz1zJrWN6RZ6q0a+SSK8HdgSiKSF66SBIRILFoQmapBLXRY9YyATt
|
||||
cdgg7pOd1WGCMqlOnhL56c8X5n+j/LGM5hc9PaEJA5vru7EBrnbxCkg0n8yp4Swc
|
||||
8KFV5IiZ5D8t03AHjrXLQg8/HRzTuFRJJ1nJmc+FbnjT
|
||||
-----END CERTIFICATE-----
|
||||
15
e2e_test/keys/testdata/ca.key
vendored
Normal file
15
e2e_test/keys/testdata/ca.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDnSTDsRx4UJmaTWHOAZasfN2O37wMcRez7LDM2qfQ8nlXnEAAZ
|
||||
4Pc51itOycWN1nclNVb489i9J8ALgRKzNumSkfl1sCgJoDds75AC3oRRCbhnEP3L
|
||||
u4mysxyOtYZNsdST8GBCP0m42tWa4W2ditpA44uU4x8opAX2qY919nVLNwIDAQAB
|
||||
AoGAaYmTYm29QvKW4et9oPxDjpYG0bqlz7P0xFRR9kKtKTATAMHjWeu2xFR/JI+b
|
||||
rvJLIdZqHmWe5AmMb3NxZgfLonEB71ohaKQha1L8Vc7aoedRheJvqqaNr+ZxoCMO
|
||||
8xcjsaMYxLEVt0tg6XyKyEhi1/hOufFZ4BSng4oQbrpaNIkCQQD6hPEzzPZtMEEe
|
||||
eRdwTVUIStKFMQbRdwZ5Oc7pyDk2U+SFRJiqkBkqnmFekcf2UgbBQxem+GMhWNgE
|
||||
LItKy/wVAkEA7FiHxbzn2msaE3hZCWudtnXqmJNuPO0zJ5icXe2svwmwPfLA/rm9
|
||||
iazCuyzyK67J8IG9QgIjQFYXtQbMr2chGwJBAMm3dghBx0LQEf8Zfdf9TLSqmqyI
|
||||
d3b+IgZGl+cCQ58NGfp863ibIsiAUuK0+4/JKItBHLBjXF6jjPx/aYFGkqkCQH4w
|
||||
GnXCEYx1qJuCow87jR4xQQsrlC0lfC2E9t/TmWr6UkYRCWg3ZXJPcj0bl0Upcppd
|
||||
ut22ZHniPZAizEBOcMcCQQDnMEOxufxhMsx2NC8yON/noewLINqKcbkMsl6DjvJl
|
||||
+wLbQmzJ0j+uIBgdpsj4rWnEr7GxoL2eWG44QDUBco79
|
||||
-----END RSA PRIVATE KEY-----
|
||||
15
e2e_test/keys/testdata/jws.key
vendored
Normal file
15
e2e_test/keys/testdata/jws.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXwIBAAKBgQCZukkN1GxMlNXkpOZxCnvCF874/rn1sNKzO98fOwmBRPG2+m/c
|
||||
yqBY7t2L2nihqz3+GZTiHmzSrBzMAGPVW1qGmk9KYg3m7akz9SiCxdoUkgM9MCCp
|
||||
X/s8IhgtXkyoKFPcGdwHblDl/3aJG02b6TAQD8vTNQAKoKw7L0FST+pvRwIDAQAB
|
||||
AoGBAJT1fXR5MbfDQL+dSe6fSex5RYTgzzDTdldW3I1Wl487Tz0OzvYTIe0LCIJL
|
||||
4DhHxnpCL5IsCSbav8ytVA+ZxczHpEW6UxbalXt5UfgFu0joTrdoGxDcVWgUCW3J
|
||||
Olbln0lOP55wViKh509gt45Za3VxJrNul3khVfVj7qGG9cKBAkEAxwtT8LxwqTYF
|
||||
nqoeZvPp15JAqlgdk38ttJa4KEqvpBTSxNIXkL9T5gJ+irKZAzxlz/U7bhn5mw6E
|
||||
3xFiljOXpQJBAMW3XRFOjgNBXNjbt81wREF5LdZl9EI8cRMSH6xljt2uwSqw4EG3
|
||||
76gFvccUd+WnfspFQZVypSSD4pWzsAqh13sCQQCA9BLW5Y7r4ab0a2y08JNwaT1h
|
||||
3yKSO5QF6pu25uQyHpeKkj5YNcyKONV40EqXsRqZB10QcN2omlh1GJNRkm1NAkEA
|
||||
qV3lr4mnRUqcinfM/4MINT3k8h/sGUFFa5y+3SMyOtwURMm3kRRLi5c/dmYmPug4
|
||||
SHUDNU48AQeo9awzRShWOQJBAJdw+cfRgi4fo3HY33uZdFa1T9G+qTA2ijhco6O3
|
||||
8tOc0yOFEtPNXM87MsHGIQP3ZCLfIY1gs2O3WCTFbPxR4rc=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
2
e2e_test/keys/testdata/openssl.cnf
vendored
2
e2e_test/keys/testdata/openssl.cnf
vendored
@@ -10,7 +10,7 @@ new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
default_days = 365
|
||||
default_days = 3650
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
|
||||
52
e2e_test/keys/testdata/server.crt
vendored
Normal file
52
e2e_test/keys/testdata/server.crt
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
Certificate:
|
||||
Data:
|
||||
Version: 3 (0x2)
|
||||
Serial Number: 0 (0x0)
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
Issuer: CN=Hello CA
|
||||
Validity
|
||||
Not Before: Aug 18 06:00:06 2019 GMT
|
||||
Not After : Aug 15 06:00:06 2029 GMT
|
||||
Subject: CN=localhost
|
||||
Subject Public Key Info:
|
||||
Public Key Algorithm: rsaEncryption
|
||||
Public-Key: (1024 bit)
|
||||
Modulus:
|
||||
00:d6:4e:eb:3a:cb:25:f9:7e:92:22:f2:63:99:da:
|
||||
08:05:8b:a3:e7:d3:fd:71:3e:bd:da:c5:d5:63:b7:
|
||||
d3:7b:f8:cd:1a:2e:5c:a2:4f:48:98:c2:b4:da:e8:
|
||||
1e:d3:d7:8f:d8:ee:a9:70:d0:9d:4f:f4:8d:95:e5:
|
||||
8e:9a:71:b6:80:aa:0b:cb:28:1d:f6:0d:7e:aa:78:
|
||||
bf:30:e6:58:d7:6b:92:8f:19:1c:7d:95:f8:d5:2f:
|
||||
8c:58:49:98:88:05:50:88:80:a9:77:c4:16:b4:c1:
|
||||
00:45:1e:d3:d0:ed:98:4d:f7:a3:5d:f1:82:cb:a5:
|
||||
4d:19:64:4d:43:db:13:d4:17
|
||||
Exponent: 65537 (0x10001)
|
||||
X509v3 extensions:
|
||||
X509v3 Basic Constraints:
|
||||
CA:FALSE
|
||||
X509v3 Key Usage:
|
||||
Digital Signature, Non Repudiation, Key Encipherment
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:localhost
|
||||
Signature Algorithm: sha256WithRSAEncryption
|
||||
5a:5c:5e:8b:de:82:86:f4:98:40:0e:cf:c5:51:fe:89:46:49:
|
||||
f0:26:d2:a5:06:e3:91:43:c1:f8:b2:ad:b7:a1:23:13:1a:80:
|
||||
45:00:51:70:b6:06:63:c6:a8:c8:22:5d:1b:00:e0:4a:8c:2e:
|
||||
ce:b4:da:b1:89:8a:d2:d0:e3:eb:0f:16:34:45:a1:bd:64:5c:
|
||||
48:41:8c:0a:bf:66:be:1c:a8:35:47:ce:b0:dc:c8:4f:5e:c1:
|
||||
ec:ef:21:fb:45:55:95:e3:99:40:46:0b:6c:8a:b3:d5:f0:bf:
|
||||
39:a4:ba:c4:d7:58:88:58:08:07:98:59:6e:ca:9c:08:e4:c4:
|
||||
4f:db
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBzTCCATagAwIBAgIBADANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhIZWxs
|
||||
byBDQTAeFw0xOTA4MTgwNjAwMDZaFw0yOTA4MTUwNjAwMDZaMBQxEjAQBgNVBAMM
|
||||
CWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1k7rOssl+X6S
|
||||
IvJjmdoIBYuj59P9cT692sXVY7fTe/jNGi5cok9ImMK02uge09eP2O6pcNCdT/SN
|
||||
leWOmnG2gKoLyygd9g1+qni/MOZY12uSjxkcfZX41S+MWEmYiAVQiICpd8QWtMEA
|
||||
RR7T0O2YTfejXfGCy6VNGWRNQ9sT1BcCAwEAAaMwMC4wCQYDVR0TBAIwADALBgNV
|
||||
HQ8EBAMCBeAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4GB
|
||||
AFpcXovegob0mEAOz8VR/olGSfAm0qUG45FDwfiyrbehIxMagEUAUXC2BmPGqMgi
|
||||
XRsA4EqMLs602rGJitLQ4+sPFjRFob1kXEhBjAq/Zr4cqDVHzrDcyE9ewezvIftF
|
||||
VZXjmUBGC2yKs9XwvzmkusTXWIhYCAeYWW7KnAjkxE/b
|
||||
-----END CERTIFICATE-----
|
||||
15
e2e_test/keys/testdata/server.key
vendored
Normal file
15
e2e_test/keys/testdata/server.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXgIBAAKBgQDWTus6yyX5fpIi8mOZ2ggFi6Pn0/1xPr3axdVjt9N7+M0aLlyi
|
||||
T0iYwrTa6B7T14/Y7qlw0J1P9I2V5Y6acbaAqgvLKB32DX6qeL8w5ljXa5KPGRx9
|
||||
lfjVL4xYSZiIBVCIgKl3xBa0wQBFHtPQ7ZhN96Nd8YLLpU0ZZE1D2xPUFwIDAQAB
|
||||
AoGBAJhNR7Dl1JwFzndViWE6aP7/6UEFEBWeADDs7aTLbFmrTJ+xmRWkgLRHk14L
|
||||
HnVwuYLywaoyJ8o9wy1nEbxC2e4zWZ94d351MQf3/komCXDBzEsktfAcNsAFnMmS
|
||||
HZuGXfhi0FYWoftpIGxUmEBmQRcq0ctycbLves6TY3y+oajpAkEA8UHmSr/zsM3E
|
||||
XQXPp2BCAvRrTH/njk4R0jwB29Bi89gt/XDD4uvfWbHw7TZxnZuCpWisnxpMPIwa
|
||||
1rjqIQmhEwJBAONncQUOxwYCIuvraIhV0QtkIUa+YpTvAxP8ZNXx+agtHmHG2TTf
|
||||
kGv2YddvjxXZItN/FZOzUGm9OptaeLRTpW0CQHO8CEzNnoqve0agtgf2LlSaiiqt
|
||||
pRhoLTZsYPvhEMcnapCNGvtt6bxul0REfOZ9poPRHhZJGE9naqydEnv80Y8CQQC3
|
||||
pxLfws95SsBpR/VkJepuCK/XMmrrXRxfR7coEgROjiG7VZyV1vgMOS9Ljg1A19wI
|
||||
cto6LtcCjpCGZsqU1/kBAkEAv2tXBts3vuIjguZNMz7KLWmu3zG2SQRaqdEZwL+R
|
||||
DQmD5tbI6gEtd5OmgmSiW8A4mpfgFYvG7Um2fwi7TTXtSA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,27 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/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
|
||||
}
|
||||
@@ -5,20 +5,19 @@ import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"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",
|
||||
@@ -124,8 +125,6 @@ func TestCmd_Run_Login(t *testing.T) {
|
||||
serverURL, server := p.startServer(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
@@ -135,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",
|
||||
@@ -166,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",
|
||||
@@ -197,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",
|
||||
@@ -230,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",
|
||||
@@ -258,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",
|
||||
@@ -308,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) {
|
||||
9
go.mod
9
go.mod
@@ -3,20 +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
9
go.sum
@@ -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=
|
||||
|
||||
2
main.go
2
main.go
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
)
|
||||
|
||||
var version = "HEAD"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
// Package credentialplugin provides models for the credential plugin.
|
||||
package credentialplugin
|
||||
|
||||
import "time"
|
||||
|
||||
// TokenCache represents a token object cached.
|
||||
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
|
||||
}
|
||||
@@ -4,10 +4,21 @@ metadata:
|
||||
name: oidc-login
|
||||
spec:
|
||||
homepage: https://github.com/int128/kubelogin
|
||||
shortDescription: kubectl integration for OpenID Connect authentication
|
||||
shortDescription: Log in to the OpenID Connect provider
|
||||
description: |
|
||||
Kubelogin integrates browser based authentication with kubectl.
|
||||
You do not need to manually set an ID token and refresh token to the kubeconfig.
|
||||
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
|
||||
|
||||
## Credential plugin mode
|
||||
kubectl executes oidc-login before calling the Kubernetes APIs.
|
||||
oidc-login automatically opens the browser and you can log in to the provider.
|
||||
After authentication, kubectl gets the token from oidc-login and you can access the cluster.
|
||||
See https://github.com/int128/kubelogin#credential-plugin-mode for more.
|
||||
|
||||
## Standalone mode
|
||||
Run `kubectl oidc-login`.
|
||||
It automatically opens the browser and you can log in to the provider.
|
||||
After authentication, it writes the token to the kubeconfig and you can access the cluster.
|
||||
See https://github.com/int128/kubelogin#standalone-mode for more.
|
||||
|
||||
caveats: |
|
||||
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
|
||||
|
||||
74
pkg/adaptors/cmd/cmd.go
Normal file
74
pkg/adaptors/cmd/cmd.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/spf13/cobra"
|
||||
"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(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
|
||||
|
||||
# Login to the provider using the resource owner password credentials flow.
|
||||
%[1]s --username USERNAME --password PASSWORD
|
||||
|
||||
# Run as a credential plugin.
|
||||
%[1]s get-token --oidc-issuer-url=https://issuer.example.com`
|
||||
|
||||
var defaultListenPort = []int{8000, 18000}
|
||||
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
|
||||
|
||||
// Cmd provides interaction with command line interface (CLI).
|
||||
type Cmd struct {
|
||||
Root *Root
|
||||
GetToken *GetToken
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
// Run parses the command line arguments and executes the specified use-case.
|
||||
// It returns an exit code, that is 0 on success or 1 on error.
|
||||
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
executable := filepath.Base(args[0])
|
||||
|
||||
rootCmd := cmd.Root.New(ctx, executable)
|
||||
rootCmd.Version = version
|
||||
rootCmd.SilenceUsage = true
|
||||
rootCmd.SilenceErrors = true
|
||||
|
||||
getTokenCmd := cmd.GetToken.New(ctx)
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
cmd.Logger.Printf("%s version %s", executable, version)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
rootCmd.SetArgs(args[1:])
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
cmd.Logger.Printf("error: %s", err)
|
||||
cmd.Logger.V(1).Infof("stacktrace: %+v", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"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{
|
||||
ListenPort: defaultListenPort,
|
||||
TokenCacheFilename: defaultTokenCache,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
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,28 +139,31 @@ 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{
|
||||
TokenCacheFilename: defaultTokenCache,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: []int{10080, 20080},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
Do(ctx, credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: []int{10080, 20080},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
|
||||
|
||||
cmd := Cmd{
|
||||
GetToken: getToken,
|
||||
Logger: logger,
|
||||
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 {
|
||||
82
pkg/adaptors/cmd/get_token.go
Normal file
82
pkg/adaptors/cmd/get_token.go
Normal 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
86
pkg/adaptors/cmd/root.go
Normal 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
|
||||
}
|
||||
@@ -4,24 +4,35 @@ package credentialplugin
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/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",
|
||||
@@ -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)
|
||||
}
|
||||
26
adaptors/env/env.go → pkg/adaptors/env/env.go
vendored
26
adaptors/env/env.go → pkg/adaptors/env/env.go
vendored
@@ -6,17 +6,31 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/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
58
pkg/adaptors/env/mock_env/mock_env.go
vendored
Normal 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)
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
//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(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
|
||||
|
||||
@@ -26,3 +43,5 @@ type OIDCConfig struct {
|
||||
IDToken string // (optional) id-token
|
||||
RefreshToken string // (optional) refresh-token
|
||||
}
|
||||
|
||||
type Kubeconfig struct{}
|
||||
@@ -3,13 +3,12 @@ package kubeconfig
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/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"],
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/int128/kubelogin/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",
|
||||
},
|
||||
}
|
||||
59
pkg/adaptors/kubeconfig/mock_kubeconfig/mock_kubeconfig.go
Normal file
59
pkg/adaptors/kubeconfig/mock_kubeconfig/mock_kubeconfig.go
Normal 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)
|
||||
}
|
||||
@@ -3,12 +3,11 @@ package kubeconfig
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/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)
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/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",
|
||||
60
pkg/adaptors/logger/logger.go
Normal file
60
pkg/adaptors/logger/logger.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Logger.
|
||||
var Set = wire.NewSet(
|
||||
New,
|
||||
)
|
||||
|
||||
// New returns a Logger with the standard log.Logger and klog.
|
||||
func New() Interface {
|
||||
return &Logger{
|
||||
goLogger: log.New(os.Stderr, "", 0),
|
||||
}
|
||||
}
|
||||
|
||||
type Interface interface {
|
||||
AddFlags(f *pflag.FlagSet)
|
||||
Printf(format string, args ...interface{})
|
||||
V(level int) Verbose
|
||||
IsEnabled(level int) bool
|
||||
}
|
||||
|
||||
type Verbose interface {
|
||||
Infof(format string, args ...interface{})
|
||||
}
|
||||
|
||||
type goLogger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger provides logging facility using log.Logger and klog.
|
||||
type Logger struct {
|
||||
goLogger
|
||||
}
|
||||
|
||||
// AddFlags adds the flags such as -v.
|
||||
func (*Logger) AddFlags(f *pflag.FlagSet) {
|
||||
gf := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
klog.InitFlags(gf)
|
||||
f.AddGoFlagSet(gf)
|
||||
}
|
||||
|
||||
// V returns a logger enabled only if the level is enabled.
|
||||
func (*Logger) V(level int) Verbose {
|
||||
return klog.V(klog.Level(level))
|
||||
}
|
||||
|
||||
// IsEnabled returns true if the level is enabled.
|
||||
func (*Logger) IsEnabled(level int) bool {
|
||||
return bool(klog.V(klog.Level(level)))
|
||||
}
|
||||
46
pkg/adaptors/logger/mock_logger/logger.go
Normal file
46
pkg/adaptors/logger/mock_logger/logger.go
Normal 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
|
||||
}
|
||||
161
pkg/adaptors/oidc/client.go
Normal file
161
pkg/adaptors/oidc/client.go
Normal 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
|
||||
}
|
||||
61
pkg/adaptors/oidc/decoder.go
Normal file
61
pkg/adaptors/oidc/decoder.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"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) (*DecodedIDToken, error) {
|
||||
parts := strings.Split(t, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, xerrors.Errorf("token contains an invalid number of segments")
|
||||
}
|
||||
b, err := jwt.DecodeSegment(parts[1])
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the token: %w", err)
|
||||
}
|
||||
var claims jwt.StandardClaims
|
||||
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&claims); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
|
||||
}
|
||||
var rawClaims map[string]interface{}
|
||||
if err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&rawClaims); err != nil {
|
||||
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
|
||||
}
|
||||
return &DecodedIDToken{
|
||||
IDTokenExpiry: time.Unix(claims.ExpiresAt, 0),
|
||||
IDTokenClaims: dumpRawClaims(rawClaims),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dumpRawClaims(rawClaims map[string]interface{}) map[string]string {
|
||||
claims := make(map[string]string)
|
||||
for k, v := range rawClaims {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
claims[k] = fmt.Sprintf("%.f", v.(float64))
|
||||
default:
|
||||
claims[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
return claims
|
||||
}
|
||||
87
pkg/adaptors/oidc/decoder_test.go
Normal file
87
pkg/adaptors/oidc/decoder_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
func TestDecoder_DecodeIDToken(t *testing.T) {
|
||||
var decoder Decoder
|
||||
|
||||
t.Run("ValidToken", func(t *testing.T) {
|
||||
expiry := time.Now().Round(time.Second)
|
||||
idToken := newIDToken(t, "https://issuer.example.com", expiry)
|
||||
decodedToken, err := decoder.DecodeIDToken(idToken)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeIDToken error: %s", err)
|
||||
}
|
||||
if decodedToken.IDTokenExpiry != expiry {
|
||||
t.Errorf("IDTokenExpiry wants %s but %s", expiry, decodedToken.IDTokenExpiry)
|
||||
}
|
||||
t.Logf("IDTokenClaims=%+v", decodedToken.IDTokenClaims)
|
||||
})
|
||||
t.Run("InvalidToken", func(t *testing.T) {
|
||||
decodedToken, err := decoder.DecodeIDToken("HEADER.INVALID_TOKEN.SIGNATURE")
|
||||
if err == nil {
|
||||
t.Errorf("error wants non-nil but nil")
|
||||
} else {
|
||||
t.Logf("expected error: %+v", err)
|
||||
}
|
||||
if decodedToken != nil {
|
||||
t.Errorf("decodedToken wants nil but %+v", decodedToken)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
claims := struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
}{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: expiry.Unix(),
|
||||
},
|
||||
Nonce: "NONCE",
|
||||
Groups: []string{"admin", "users"},
|
||||
EmailVerified: false,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(readPrivateKey(t, "testdata/jws.key"))
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func readPrivateKey(t *testing.T, name string) *rsa.PrivateKey {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the file: %s", err)
|
||||
}
|
||||
block, rest := pem.Decode(b)
|
||||
if block == nil {
|
||||
t.Fatalf("could not decode PEM")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
t.Fatalf("PEM should contain single key but multiple keys")
|
||||
}
|
||||
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse the key: %s", err)
|
||||
}
|
||||
return k
|
||||
}
|
||||
122
pkg/adaptors/oidc/factory.go
Normal file
122
pkg/adaptors/oidc/factory.go
Normal 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
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
package tls
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/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")
|
||||
}
|
||||
42
pkg/adaptors/oidc/logging/transport.go
Normal file
42
pkg/adaptors/oidc/logging/transport.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
levelDumpHeaders = 2
|
||||
levelDumpBody = 3
|
||||
)
|
||||
|
||||
type Transport struct {
|
||||
Base http.RoundTripper
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if !t.Logger.IsEnabled(levelDumpHeaders) {
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
|
||||
reqDump, err := httputil.DumpRequestOut(req, t.Logger.IsEnabled(levelDumpBody))
|
||||
if err != nil {
|
||||
t.Logger.V(levelDumpHeaders).Infof("could not dump the request: %s", err)
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
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.Logger.IsEnabled(levelDumpBody))
|
||||
if err != nil {
|
||||
t.Logger.V(levelDumpHeaders).Infof("could not dump the response: %s", err)
|
||||
return resp, err
|
||||
}
|
||||
t.Logger.V(levelDumpHeaders).Infof("%s", string(respDump))
|
||||
return resp, err
|
||||
}
|
||||
49
pkg/adaptors/oidc/logging/transport_test.go
Normal file
49
pkg/adaptors/oidc/logging/transport_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger/mock_logger"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
t.req = req
|
||||
return t.resp, nil
|
||||
}
|
||||
|
||||
func TestLoggingTransport_RoundTrip(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
|
||||
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
|
||||
Host: example.com
|
||||
|
||||
dummy`)), req)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a response: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
transport := &Transport{
|
||||
Base: &mockTransport{resp: resp},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
gotResp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("RoundTrip error: %s", err)
|
||||
}
|
||||
if gotResp != resp {
|
||||
t.Errorf("resp wants %v but %v", resp, gotResp)
|
||||
}
|
||||
}
|
||||
146
pkg/adaptors/oidc/mock_oidc/mock_oidc.go
Normal file
146
pkg/adaptors/oidc/mock_oidc/mock_oidc.go
Normal 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)
|
||||
}
|
||||
15
pkg/adaptors/oidc/oidc.go
Normal file
15
pkg/adaptors/oidc/oidc.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
//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(FactoryInterface), new(*Factory)),
|
||||
wire.Struct(new(Decoder)),
|
||||
wire.Bind(new(DecoderInterface), new(*Decoder)),
|
||||
)
|
||||
8
pkg/adaptors/oidc/testdata/Makefile
vendored
Normal file
8
pkg/adaptors/oidc/testdata/Makefile
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
all: jws.key
|
||||
|
||||
jws.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -v jws.key
|
||||
15
pkg/adaptors/oidc/testdata/jws.key
vendored
Normal file
15
pkg/adaptors/oidc/testdata/jws.key
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQCrH34yA/f/sBOUlkYnRtd2jgDZ3WivhidqvoQaa73xqTazbkn6
|
||||
GZ9r7jx0CGLRV2bmErj2WoyT54yrhezrKh0YXAHlrwLdsmV4dwiV0lOfUJd9P/vF
|
||||
e2hiAWv4CcO9ZuNkTsrxM5W8Wdj2tjqOvsIn4We+HWPkpknT7VtT5RrumwIDAQAB
|
||||
AoGAFqy5oA7+kZbXQV0YNqQgcMkoO7Ym5Ps1xeMwxf94z8jIQsZebxFuGnMa95UU
|
||||
4wBd1ias85fUANUxwpigaBjQee5Hk+dnfUe1snUWYNm9H6tKrXEF8ajer3a2knEv
|
||||
GfK0CSEumFougfW2xG88ChGTS60wc+MIRfXERCvWpGm/5EECQQDdv5IBSi89g/R1
|
||||
5AGZKFCoqr6Zw5bWEKPzCCYJZzncR1ER9vP2AnMExM8Io/87WYvmpZIUrXJvQYm8
|
||||
hkfVOcBZAkEAxY4VcqmRWru3zmnbj21MwcwtgESaONkWsHeYs1C/Y/3zt7TuelYz
|
||||
ZJ9aUuUsaiJLEs9Y26nMt0L0snWGr2noEwJBANaDp1PWFyMUTt3pB17JcFXqb15C
|
||||
pt1I1cGapWk9Uez1lMijNNhNAEWhuoKqW5Nnif5DN7EHJYfZR8x3vm/YYWkCQHAA
|
||||
0iAkCwjKDLe2RIjYiwAE5ncmbdl1GuwJokVnrlrei+LHbb1mSdTuk6MT006JCs8r
|
||||
R1GivzHXgCv9fdLN1IkCQHxRvv9RPND80eEkdMv4qu0s22OLRhLQ/pb+YeT5Cjjv
|
||||
pJYWKrvXdRZcuNde9JiiTgK2UW1wM8KeD/EGvK2yF6M=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,9 +1,8 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: ca1.crt ca1.crt.base64 ca2.crt ca2.crt.base64 ca3.crt ca3.crt.base64
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -v *.key *.csr *.crt *.base64
|
||||
-rm -v *.key *.csr *.crt *.base64
|
||||
|
||||
%.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
59
pkg/adaptors/tokencache/mock_tokencache/mock_tokencache.go
Normal file
59
pkg/adaptors/tokencache/mock_tokencache/mock_tokencache.go
Normal 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)
|
||||
}
|
||||
81
pkg/adaptors/tokencache/tokencache.go
Normal file
81
pkg/adaptors/tokencache/tokencache.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package tokencache
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
"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(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 Key) (*TokenCache, error) {
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not open file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
d := json.NewDecoder(f)
|
||||
var c 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 Key, cache TokenCache) error {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return xerrors.Errorf("could not create directory %s: %w", dir, err)
|
||||
}
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not create file %s: %w", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
e := json.NewEncoder(f)
|
||||
if err := e.Encode(&cache); err != nil {
|
||||
return xerrors.Errorf("could not encode json to file %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func computeFilename(key Key) string {
|
||||
s := sha256.New()
|
||||
_, _ = s.Write([]byte(key.IssuerURL))
|
||||
_, _ = s.Write([]byte{0x00})
|
||||
_, _ = s.Write([]byte(key.ClientID))
|
||||
return hex.EncodeToString(s.Sum(nil))
|
||||
}
|
||||
@@ -7,10 +7,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
)
|
||||
|
||||
func TestRepository_Read(t *testing.T) {
|
||||
func TestRepository_FindByKey(t *testing.T) {
|
||||
var r Repository
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
@@ -23,24 +22,28 @@ func TestRepository_Read(t *testing.T) {
|
||||
t.Errorf("could not clean up the temp dir: %s", err)
|
||||
}
|
||||
}()
|
||||
key := Key{
|
||||
IssuerURL: "YOUR_ISSUER",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}
|
||||
json := `{"id_token":"YOUR_ID_TOKEN","refresh_token":"YOUR_REFRESH_TOKEN"}`
|
||||
filename := filepath.Join(dir, "token-cache")
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
if err := ioutil.WriteFile(filename, []byte(json), 0600); err != nil {
|
||||
t.Fatalf("could not write to the temp file: %s", err)
|
||||
}
|
||||
|
||||
tokenCache, err := r.Read(filename)
|
||||
tokenCache, err := r.FindByKey(dir, key)
|
||||
if err != nil {
|
||||
t.Errorf("err wants nil but %+v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_Write(t *testing.T) {
|
||||
func TestRepository_Save(t *testing.T) {
|
||||
var r Repository
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
@@ -54,12 +57,16 @@ func TestRepository_Write(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
filename := filepath.Join(dir, "token-cache")
|
||||
tokenCache := credentialplugin.TokenCache{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
|
||||
if err := r.Write(filename, tokenCache); err != nil {
|
||||
key := Key{
|
||||
IssuerURL: "YOUR_ISSUER",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
filename := filepath.Join(dir, computeFilename(key))
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the token cache file: %s", err)
|
||||
54
pkg/di/di.go
Normal file
54
pkg/di/di.go
Normal file
@@ -0,0 +1,54 @@
|
||||
//+build wireinject
|
||||
|
||||
// Package di provides dependency injection.
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/cmd"
|
||||
credentialPluginAdaptor "github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
credentialPluginUseCase "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
)
|
||||
|
||||
// NewCmd returns an instance of adaptors.Cmd.
|
||||
func NewCmd() cmd.Interface {
|
||||
wire.Build(
|
||||
// use-cases
|
||||
auth.Set,
|
||||
wire.Value(auth.DefaultLocalServerReadyFunc),
|
||||
standalone.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
|
||||
// adaptors
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
credentialPluginAdaptor.Set,
|
||||
oidc.Set,
|
||||
logger.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
|
||||
func NewCmdForHeadless(logger.Interface, auth.LocalServerReadyFunc, credentialPluginAdaptor.Interface) cmd.Interface {
|
||||
wire.Build(
|
||||
auth.Set,
|
||||
standalone.Set,
|
||||
credentialPluginUseCase.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
tokencache.Set,
|
||||
oidc.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
112
pkg/di/wire_gen.go
Normal file
112
pkg/di/wire_gen.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate wire
|
||||
//+build !wireinject
|
||||
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/env"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/usecases/auth"
|
||||
credentialplugin2 "github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
)
|
||||
|
||||
// Injectors from di.go:
|
||||
|
||||
func NewCmd() cmd.Interface {
|
||||
loggerInterface := logger.New()
|
||||
factory := &oidc.Factory{
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
decoder := &oidc.Decoder{}
|
||||
envEnv := &env.Env{}
|
||||
localServerReadyFunc := _wireLocalServerReadyFuncValue
|
||||
authentication := &auth.Authentication{
|
||||
OIDCFactory: factory,
|
||||
OIDCDecoder: decoder,
|
||||
Env: envEnv,
|
||||
Logger: loggerInterface,
|
||||
LocalServerReadyFunc: localServerReadyFunc,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
|
||||
standaloneStandalone := &standalone.Standalone{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
root := &cmd.Root{
|
||||
Standalone: standaloneStandalone,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
interaction := &credentialplugin.Interaction{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: interaction,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdGetToken := &cmd.GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Root: root,
|
||||
GetToken: cmdGetToken,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
|
||||
var (
|
||||
_wireLocalServerReadyFuncValue = auth.DefaultLocalServerReadyFunc
|
||||
)
|
||||
|
||||
func NewCmdForHeadless(loggerInterface logger.Interface, localServerReadyFunc auth.LocalServerReadyFunc, credentialpluginInterface credentialplugin.Interface) cmd.Interface {
|
||||
factory := &oidc.Factory{
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
decoder := &oidc.Decoder{}
|
||||
envEnv := &env.Env{}
|
||||
authentication := &auth.Authentication{
|
||||
OIDCFactory: factory,
|
||||
OIDCDecoder: decoder,
|
||||
Env: envEnv,
|
||||
Logger: loggerInterface,
|
||||
LocalServerReadyFunc: localServerReadyFunc,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
|
||||
standaloneStandalone := &standalone.Standalone{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
root := &cmd.Root{
|
||||
Standalone: standaloneStandalone,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
repository := &tokencache.Repository{}
|
||||
getToken := &credentialplugin2.GetToken{
|
||||
Authentication: authentication,
|
||||
TokenCacheRepository: repository,
|
||||
Interaction: credentialpluginInterface,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdGetToken := &cmd.GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Root: root,
|
||||
GetToken: cmdGetToken,
|
||||
Logger: loggerInterface,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
195
pkg/usecases/auth/auth.go
Normal file
195
pkg/usecases/auth/auth.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"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(Interface), new(*Authentication)),
|
||||
)
|
||||
|
||||
// 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: "
|
||||
|
||||
// Authentication provides the internal use-case of authentication.
|
||||
//
|
||||
// If the IDToken is not set, it performs the authentication flow.
|
||||
// If the IDToken is valid, it does nothing.
|
||||
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
|
||||
// If the RefreshToken has expired, it performs the authentication flow.
|
||||
//
|
||||
// The authentication flow is determined as:
|
||||
//
|
||||
// If the Username is not set, it performs the authorization code flow.
|
||||
// Otherwise, it performs the resource owner password credentials flow.
|
||||
// If the Password is not set, it asks a password by the prompt.
|
||||
//
|
||||
type Authentication struct {
|
||||
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 Input) (*Output, error) {
|
||||
if in.OIDCConfig.IDToken != "" {
|
||||
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.
|
||||
token, err := u.OIDCDecoder.DecodeIDToken(in.OIDCConfig.IDToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("invalid token and you need to remove the cache: %w", err)
|
||||
}
|
||||
if token.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
|
||||
u.Logger.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,
|
||||
IDTokenExpiry: token.IDTokenExpiry,
|
||||
IDTokenClaims: token.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.V(1).Infof("you have an expired token at %s", token.IDTokenExpiry)
|
||||
}
|
||||
|
||||
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 OIDCFactory client: %w", err)
|
||||
}
|
||||
|
||||
if in.OIDCConfig.RefreshToken != "" {
|
||||
u.Logger.V(1).Infof("refreshing the token")
|
||||
out, err := client.Refresh(ctx, in.OIDCConfig.RefreshToken)
|
||||
if err == nil {
|
||||
return &Output{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.V(1).Infof("could not refresh the token: %s", err)
|
||||
}
|
||||
|
||||
if in.Username == "" {
|
||||
return u.doAuthCodeFlow(ctx, in, client)
|
||||
}
|
||||
return u.doPasswordCredentialsFlow(ctx, in, client)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 &Output{
|
||||
IDToken: tokenSet.IDToken,
|
||||
RefreshToken: tokenSet.RefreshToken,
|
||||
IDTokenExpiry: tokenSet.IDTokenExpiry,
|
||||
IDTokenClaims: tokenSet.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/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,35 +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",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "VALID_ID_TOKEN"}).
|
||||
Return(&adaptors.OIDCVerifyOut{
|
||||
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("VALID_ID_TOKEN").
|
||||
Return(&oidc.DecodedIDToken{
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.OIDCConfig,
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
u := Authentication{
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
OIDCFactory: mock_oidc.NewMockFactoryInterface(ctrl),
|
||||
OIDCDecoder: mockOIDCDecoder,
|
||||
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,
|
||||
@@ -256,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",
|
||||
@@ -264,38 +301,38 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
|
||||
Return(&adaptors.OIDCVerifyOut{
|
||||
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("EXPIRED_ID_TOKEN").
|
||||
Return(&oidc.DecodedIDToken{
|
||||
IDTokenExpiry: pastTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
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,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
OIDCFactory: mockOIDCFactory,
|
||||
OIDCDecoder: mockOIDCDecoder,
|
||||
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,
|
||||
@@ -310,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",
|
||||
@@ -319,43 +356,41 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
},
|
||||
}
|
||||
mockOIDCClient := mock_adaptors.NewMockOIDCClient(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{IDToken: "EXPIRED_ID_TOKEN"}).
|
||||
Return(&adaptors.OIDCVerifyOut{
|
||||
mockOIDCDecoder := mock_oidc.NewMockDecoderInterface(ctrl)
|
||||
mockOIDCDecoder.EXPECT().
|
||||
DecodeIDToken("EXPIRED_ID_TOKEN").
|
||||
Return(&oidc.DecodedIDToken{
|
||||
IDTokenExpiry: pastTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
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,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
OIDCFactory: mockOIDCFactory,
|
||||
OIDCDecoder: mockOIDCDecoder,
|
||||
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,
|
||||
48
pkg/usecases/auth/mock_auth/mock_auth.go
Normal file
48
pkg/usecases/auth/mock_auth/mock_auth.go
Normal 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)
|
||||
}
|
||||
99
pkg/usecases/credentialplugin/get_token.go
Normal file
99
pkg/usecases/credentialplugin/get_token.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Package credentialplugin provides the use-cases for running as a client-go credentials plugin.
|
||||
//
|
||||
// See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||
package credentialplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"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(Interface), new(*GetToken)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Do(ctx context.Context, in Input) error
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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.V(1).Infof("could not find a token cache: %s", err)
|
||||
cache = &tokencache.TokenCache{}
|
||||
}
|
||||
out, err := u.Authentication.Do(ctx, auth.Input{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
IDToken: cache.IDToken,
|
||||
RefreshToken: cache.RefreshToken,
|
||||
},
|
||||
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 got a valid token until %s", out.IDTokenExpiry)
|
||||
cache := tokencache.TokenCache{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
}
|
||||
if err := u.TokenCacheRepository.Save(in.TokenCacheDir, cacheKey, cache); err != nil {
|
||||
return xerrors.Errorf("could not write the token cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -6,11 +6,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/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,21 +25,21 @@ func TestGetToken_Do(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
in := Input{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheDir: "/path/to/token-cache",
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
CACertFilename: "/path/to/cert",
|
||||
SkipTLSVerify: true,
|
||||
}
|
||||
mockAuthentication := mock_usecases.NewMockAuthentication(ctrl)
|
||||
mockAuthentication := 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,22 +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().
|
||||
Read("/path/to/token-cache").
|
||||
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().
|
||||
Write("/path/to/token-cache", credentialplugin.TokenCache{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_adaptors.NewMockCredentialPluginInteraction(ctrl)
|
||||
Save("/path/to/token-cache",
|
||||
tokencache.Key{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
},
|
||||
tokencache.TokenCache{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
credentialPluginInteraction := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
credentialPluginInteraction.EXPECT().
|
||||
Write(credentialplugin.Output{
|
||||
Token: "YOUR_ID_TOKEN",
|
||||
@@ -74,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)
|
||||
@@ -85,15 +96,15 @@ func TestGetToken_Do(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
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",
|
||||
@@ -101,19 +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().
|
||||
Read("/path/to/token-cache").
|
||||
Return(&credentialplugin.TokenCache{
|
||||
FindByKey("/path/to/token-cache", tokencache.Key{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}).
|
||||
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",
|
||||
@@ -123,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)
|
||||
@@ -134,15 +148,15 @@ func TestGetToken_Do(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
in := usecases.GetTokenIn{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
TokenCacheFilename: "/path/to/token-cache",
|
||||
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",
|
||||
@@ -150,15 +164,18 @@ func TestGetToken_Do(t *testing.T) {
|
||||
},
|
||||
}).
|
||||
Return(nil, xerrors.New("authentication error"))
|
||||
tokenCacheRepository := mock_adaptors.NewMockTokenCacheRepository(ctrl)
|
||||
tokenCacheRepository := mock_tokencache.NewMockInterface(ctrl)
|
||||
tokenCacheRepository.EXPECT().
|
||||
Read("/path/to/token-cache").
|
||||
FindByKey("/path/to/token-cache", tokencache.Key{
|
||||
IssuerURL: "https://accounts.google.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
}).
|
||||
Return(nil, xerrors.New("file not found"))
|
||||
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")
|
||||
@@ -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)
|
||||
}
|
||||
47
pkg/usecases/standalone/mock_standalone/mock_standalone.go
Normal file
47
pkg/usecases/standalone/mock_standalone/mock_standalone.go
Normal 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)
|
||||
}
|
||||
96
pkg/usecases/standalone/standalone.go
Normal file
96
pkg/usecases/standalone/standalone.go
Normal 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
|
||||
}
|
||||
@@ -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/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/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
19
snapcraft.yaml
Normal 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
|
||||
@@ -1,139 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides the use-case of Authentication.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Authentication), "*"),
|
||||
wire.Bind(new(usecases.Authentication), 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)),
|
||||
)
|
||||
|
||||
const passwordPrompt = "Password: "
|
||||
|
||||
// Authentication provides the internal use-case of authentication.
|
||||
//
|
||||
// If the IDToken is not set, it performs the authentication flow.
|
||||
// If the IDToken is valid, it does nothing.
|
||||
// If the IDtoken has expired and the RefreshToken is set, it refreshes the token.
|
||||
// If the RefreshToken has expired, it performs the authentication flow.
|
||||
//
|
||||
// The authentication flow is determined as:
|
||||
//
|
||||
// If the Username is not set, it performs the authorization code flow.
|
||||
// Otherwise, it performs the resource owner password credentials flow.
|
||||
// If the Password is not set, it asks a password by the prompt.
|
||||
//
|
||||
type Authentication struct {
|
||||
OIDC adaptors.OIDC
|
||||
Env adaptors.Env
|
||||
Logger adaptors.Logger
|
||||
ShowLocalServerURL usecases.LoginShowLocalServerURL
|
||||
}
|
||||
|
||||
func (u *Authentication) Do(ctx context.Context, in usecases.AuthenticationIn) (*usecases.AuthenticationOut, error) {
|
||||
client, err := u.OIDC.New(ctx, adaptors.OIDCClientConfig{
|
||||
Config: in.OIDCConfig,
|
||||
CACertFilename: in.CACertFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not create an OIDC client: %w", err)
|
||||
}
|
||||
|
||||
if in.OIDCConfig.IDToken != "" {
|
||||
u.Logger.Debugf(1, "Verifying the existing token")
|
||||
out, err := client.Verify(ctx, adaptors.OIDCVerifyIn{IDToken: in.OIDCConfig.IDToken})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("you need to remove the existing token manually: %w", err)
|
||||
}
|
||||
if out.IDTokenExpiry.After(time.Now()) { //TODO: inject time service
|
||||
u.Logger.Debugf(1, "You already have a valid token")
|
||||
return &usecases.AuthenticationOut{
|
||||
AlreadyHasValidIDToken: true,
|
||||
IDToken: in.OIDCConfig.IDToken,
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.Debugf(1, "You have an expired token at %s", out.IDTokenExpiry)
|
||||
}
|
||||
|
||||
if in.OIDCConfig.RefreshToken != "" {
|
||||
u.Logger.Debugf(1, "Refreshing the token")
|
||||
out, err := client.Refresh(ctx, adaptors.OIDCRefreshIn{
|
||||
RefreshToken: in.OIDCConfig.RefreshToken,
|
||||
})
|
||||
if err == nil {
|
||||
return &usecases.AuthenticationOut{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
IDTokenExpiry: out.IDTokenExpiry,
|
||||
IDTokenClaims: out.IDTokenClaims,
|
||||
}, nil
|
||||
}
|
||||
u.Logger.Debugf(1, "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
|
||||
}
|
||||
|
||||
u.Logger.Debugf(1, "Performing the resource owner password credentials flow")
|
||||
if in.Password == "" {
|
||||
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,
|
||||
})
|
||||
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,
|
||||
}, 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)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Package credentialplugin provides the use-cases for running as a client-go credentials plugin.
|
||||
//
|
||||
// See https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||
package credentialplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/credentialplugin"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(GetToken), "*"),
|
||||
wire.Bind(new(usecases.GetToken), new(*GetToken)),
|
||||
)
|
||||
|
||||
type GetToken struct {
|
||||
Authentication usecases.Authentication
|
||||
TokenCacheRepository adaptors.TokenCacheRepository
|
||||
Interaction adaptors.CredentialPluginInteraction
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (u *GetToken) Do(ctx context.Context, in usecases.GetTokenIn) error {
|
||||
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
|
||||
|
||||
tokenCache, err := u.TokenCacheRepository.Read(in.TokenCacheFilename)
|
||||
if err != nil {
|
||||
u.Logger.Debugf(1, "could not read the token cache file: %s", err)
|
||||
tokenCache = &credentialplugin.TokenCache{}
|
||||
}
|
||||
out, err := u.Authentication.Do(ctx, usecases.AuthenticationIn{
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: in.IssuerURL,
|
||||
ClientID: in.ClientID,
|
||||
ClientSecret: in.ClientSecret,
|
||||
ExtraScopes: in.ExtraScopes,
|
||||
IDToken: tokenCache.IDToken,
|
||||
RefreshToken: tokenCache.RefreshToken,
|
||||
},
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
ListenPort: in.ListenPort,
|
||||
Username: in.Username,
|
||||
Password: in.Password,
|
||||
CACertFilename: in.CACertFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error while authentication: %w", err)
|
||||
}
|
||||
for k, v := range out.IDTokenClaims {
|
||||
u.Logger.Debugf(1, "ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
if !out.AlreadyHasValidIDToken {
|
||||
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
|
||||
if err := u.TokenCacheRepository.Write(in.TokenCacheFilename, credentialplugin.TokenCache{
|
||||
IDToken: out.IDToken,
|
||||
RefreshToken: out.RefreshToken,
|
||||
}); err != nil {
|
||||
return xerrors.Errorf("could not write the token cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := u.Interaction.Write(credentialplugin.Output{Token: out.IDToken, Expiry: out.IDTokenExpiry}); err != nil {
|
||||
return xerrors.Errorf("could not write a credential object: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_usecases/mock_usecases.go github.com/int128/kubelogin/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
|
||||
TokenCacheFilename 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
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/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, "ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
if out.AlreadyHasValidIDToken {
|
||||
u.Logger.Printf("You already have a valid token until %s", out.IDTokenExpiry)
|
||||
return nil
|
||||
}
|
||||
|
||||
u.Logger.Printf("You got a valid token until %s", out.IDTokenExpiry)
|
||||
authProvider.OIDCConfig.IDToken = out.IDToken
|
||||
authProvider.OIDCConfig.RefreshToken = out.RefreshToken
|
||||
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", authProvider.LocationOfOrigin)
|
||||
if err := u.Kubeconfig.UpdateAuthProvider(authProvider); err != nil {
|
||||
return xerrors.Errorf("could not write the token to the kubeconfig: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/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/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)
|
||||
}
|
||||
Reference in New Issue
Block a user