mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-23 21:44:03 +00:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7011f03094 | ||
|
|
6aef98cef7 | ||
|
|
93bb1d39b9 | ||
|
|
c8116e2eae | ||
|
|
f2de8dd987 | ||
|
|
915fb35bc8 | ||
|
|
51ccd70af3 | ||
|
|
c6df597fb0 | ||
|
|
ee78f6f735 | ||
|
|
6e484a2b89 | ||
|
|
8050db7e05 | ||
|
|
cd54ca0df0 | ||
|
|
45f83b0b0e | ||
|
|
51b7ca1600 | ||
|
|
83f85a9b53 | ||
|
|
d82c8a2dd1 | ||
|
|
072bee6992 | ||
|
|
5c07850a68 | ||
|
|
5c8c80f055 | ||
|
|
bc7bfabfb2 | ||
|
|
73112546de | ||
|
|
356f0d519d | ||
|
|
e84d29bc6b | ||
|
|
ae80ebf148 | ||
|
|
5942c82b5f | ||
|
|
a5f9c698ea | ||
|
|
40ef2c25b8 | ||
|
|
2422c46271 | ||
|
|
6ca0ee8013 | ||
|
|
9ac252667a | ||
|
|
000711f52e | ||
|
|
e465c4852b | ||
|
|
003badb0bc | ||
|
|
0873a193a5 | ||
|
|
5e80b1858e | ||
|
|
c816281657 | ||
|
|
0db49860f9 | ||
|
|
d70c9db036 | ||
|
|
675b5e5fff | ||
|
|
e3bfc321a2 | ||
|
|
b34d0fb32f | ||
|
|
4c61a71ed4 | ||
|
|
8a02ed0fb0 | ||
|
|
3485c5408e | ||
|
|
fb99977e98 | ||
|
|
39b441a7c2 | ||
|
|
cde5becf67 | ||
|
|
460b14a159 | ||
|
|
8436fe3494 | ||
|
|
9d2319ee2f | ||
|
|
75277378fc | ||
|
|
89a1046ce3 | ||
|
|
15d40413e4 | ||
|
|
8525ba5142 | ||
|
|
dbddd6a07f | ||
|
|
839877b45e | ||
|
|
99ed86e22e | ||
|
|
a78b746c29 | ||
|
|
187bbc203c | ||
|
|
d4b5e511bb | ||
|
|
33241b8721 | ||
|
|
b72cb63826 | ||
|
|
63fda1db0f | ||
|
|
da95fe470f | ||
|
|
4b08a49a51 | ||
|
|
9c74f3748b | ||
|
|
17e03f2abc | ||
|
|
ebef81f9d7 | ||
|
|
8d0d82fb71 | ||
|
|
b600e54a12 | ||
|
|
75317f88a1 | ||
|
|
5a794e8ceb | ||
|
|
1fe1ec4c20 | ||
|
|
7676ffbfab | ||
|
|
7e1e6a096b | ||
|
|
4d3d1c3b78 | ||
|
|
1ebdfc0e4f | ||
|
|
9c67c52b34 | ||
|
|
550396e1dd | ||
|
|
34f0578b59 | ||
|
|
604d118b68 | ||
|
|
91959e8a56 | ||
|
|
9b325a66a9 | ||
|
|
8b6257d60b | ||
|
|
d469df4978 | ||
|
|
3ae68df848 | ||
|
|
e8805f7a94 | ||
|
|
717da9d442 | ||
|
|
de176cfbaa | ||
|
|
9bf8a89577 | ||
|
|
a91c020f46 | ||
|
|
d4fb49613d | ||
|
|
64b1d52208 | ||
|
|
a298058e3f | ||
|
|
309e73d8c0 | ||
|
|
857d5dad88 | ||
|
|
455c920b65 | ||
|
|
afad46817a | ||
|
|
4f506b9f62 | ||
|
|
72bc19bc10 | ||
|
|
69bcb16e26 | ||
|
|
978a45bcf1 | ||
|
|
62b9a2158d | ||
|
|
974fc5c526 | ||
|
|
2c7d958efd | ||
|
|
16b15cd21b | ||
|
|
3213572180 | ||
|
|
b7bbcd44e1 | ||
|
|
7726ac6c51 | ||
|
|
adaeba4c24 | ||
|
|
e8acaa28b3 | ||
|
|
031f9fb81a | ||
|
|
8a7da83338 | ||
|
|
b776bac764 | ||
|
|
4bf77886a8 | ||
|
|
ea711f91b4 | ||
|
|
cfc6376f69 |
@@ -2,35 +2,37 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/int128/kubelogin
|
||||
- image: circleci/golang:1.12.3
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
- run: go get github.com/golang/lint/golint
|
||||
- run: golint
|
||||
- run: go build -v
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/int128/kubelogin
|
||||
steps:
|
||||
- checkout
|
||||
- run: go get -v -t -d ./...
|
||||
- run: go get github.com/mitchellh/gox
|
||||
- run: |
|
||||
mkdir -p ~/bin
|
||||
echo 'export PATH="$HOME/bin:$PATH"' >> $BASH_ENV
|
||||
- run: |
|
||||
curl -L -o ~/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.14.0/bin/linux/amd64/kubectl
|
||||
chmod +x ~/bin/kubectl
|
||||
- run: |
|
||||
curl -L -o ~/bin/ghcp https://github.com/int128/ghcp/releases/download/v1.3.0/ghcp_linux_amd64
|
||||
chmod +x ~/bin/ghcp
|
||||
- run: |
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b ~/bin v1.16.0
|
||||
- run: go get github.com/int128/goxzst
|
||||
- run: go get github.com/tcnksm/ghr
|
||||
- run: gox --osarch 'darwin/amd64 linux/amd64 windows/amd64 windows/386' -output 'dist/{{.Dir}}_{{.OS}}_{{.Arch}}'
|
||||
- run: ghr -u "$CIRCLE_PROJECT_USERNAME" -r "$CIRCLE_PROJECT_REPONAME" "$CIRCLE_TAG" dist
|
||||
- checkout
|
||||
# workaround for https://github.com/golang/go/issues/27925
|
||||
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
|
||||
- run: make check
|
||||
- run: make run
|
||||
- run: |
|
||||
if [ "$CIRCLE_TAG" ]; then
|
||||
make release
|
||||
fi
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
all:
|
||||
jobs:
|
||||
- build
|
||||
- release:
|
||||
- build:
|
||||
context: open-source
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /.*/
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
/.idea
|
||||
/dist
|
||||
/kubelogin
|
||||
/.kubeconfig
|
||||
/kubectl-oidc_login
|
||||
/.kubeconfig*
|
||||
|
||||
35
Makefile
Normal file
35
Makefile
Normal file
@@ -0,0 +1,35 @@
|
||||
TARGET := kubelogin
|
||||
TARGET_PLUGIN := kubectl-oidc_login
|
||||
CIRCLE_TAG ?= HEAD
|
||||
LDFLAGS := -X main.version=$(CIRCLE_TAG)
|
||||
|
||||
.PHONY: check run release clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
check:
|
||||
golangci-lint run
|
||||
$(MAKE) -C adaptors_test/keys/testdata
|
||||
go test -v -race ./...
|
||||
|
||||
$(TARGET): $(wildcard *.go)
|
||||
go build -o $@ -ldflags "$(LDFLAGS)"
|
||||
|
||||
$(TARGET_PLUGIN): $(TARGET)
|
||||
ln -sf $(TARGET) $@
|
||||
|
||||
run: $(TARGET_PLUGIN)
|
||||
-PATH=.:$(PATH) kubectl oidc-login --help
|
||||
|
||||
dist:
|
||||
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
|
||||
mv dist/gh/kubelogin.rb dist/
|
||||
|
||||
release: dist
|
||||
ghr -u "$(CIRCLE_PROJECT_USERNAME)" -r "$(CIRCLE_PROJECT_REPONAME)" "$(CIRCLE_TAG)" dist/gh/
|
||||
ghcp -u "$(CIRCLE_PROJECT_USERNAME)" -r "homebrew-$(CIRCLE_PROJECT_REPONAME)" -m "$(CIRCLE_TAG)" -C dist/ kubelogin.rb
|
||||
|
||||
clean:
|
||||
-rm $(TARGET)
|
||||
-rm $(TARGET_PLUGIN)
|
||||
-rm -r dist/
|
||||
227
README.md
227
README.md
@@ -1,153 +1,160 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)
|
||||
|
||||
`kubelogin` is a command to get an OpenID Connect (OIDC) token for `kubectl` authentication.
|
||||
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
It updates the kubeconfig file with an ID token and refresh token got from the OIDC provider.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
Download [the latest release](https://github.com/int128/kubelogin/releases) and save it as `/usr/local/bin/kubelogin`.
|
||||
You need to setup the following components:
|
||||
|
||||
You have to configure `kubectl` to authenticate with OIDC.
|
||||
See the later section for details.
|
||||
- OIDC provider
|
||||
- Kubernetes API server
|
||||
- Role for your group or user
|
||||
- kubectl authentication
|
||||
|
||||
You can install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials CLUSTER_NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
|
||||
--auth-provider-arg client-id=kubernetes \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
# Homebrew
|
||||
brew tap int128/kubelogin
|
||||
brew install kubelogin
|
||||
|
||||
# Krew
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.11.0/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
|
||||
Run `kubelogin`.
|
||||
After initial setup or when the token has been expired, just run:
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/08/10 10:36:38 Reading .kubeconfig
|
||||
2018/08/10 10:36:38 Using current context: devops.hidetake.org
|
||||
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
|
||||
2018/08/10 10:36:45 GET /
|
||||
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
|
||||
2018/08/10 10:37:08 Updated .kubeconfig
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` looks like:
|
||||
|
||||
```yaml
|
||||
# ~/.kube/config (snip)
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
idp-issuer-url: https://keycloak.example.com/auth/realms/hello
|
||||
client-id: kubernetes
|
||||
client-secret: YOUR_SECRET
|
||||
id-token: ey... # kubelogin will update ID token here
|
||||
refresh-token: ey... # kubelogin will update refresh token here
|
||||
name: oidc
|
||||
```
|
||||
|
||||
Make sure you can access to the Kubernetes cluster:
|
||||
or run as a kubectl plugin:
|
||||
|
||||
```
|
||||
% kubectl version
|
||||
Client Version: version.Info{...}
|
||||
Server Version: version.Info{...}
|
||||
% kubectl oidc-login
|
||||
```
|
||||
|
||||
It opens the browser and you can log in to the provider.
|
||||
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
|
||||
|
||||
For more, see the following documents:
|
||||
|
||||
- [Getting Started with Keycloak](docs/keycloak.md)
|
||||
- [Getting Started with Google Identity Platform](docs/google.md)
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
If you are using other platforms, please contribute documents via pull requests.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
You can set the following environment variable:
|
||||
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).
|
||||
|
||||
- `KUBECONFIG` - Path to the config. Defaults to `~/.kube/config`.
|
||||
Kubelogin supports the following options.
|
||||
|
||||
|
||||
## Prerequisite
|
||||
|
||||
You have to setup your OIDC identity provider and Kubernetes cluster.
|
||||
|
||||
### 1. Setup OIDC Identity Provider
|
||||
|
||||
This tutorial assumes you have created an OIDC client with the following:
|
||||
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/hello`
|
||||
- Client ID: `kubernetes`
|
||||
- Client Secret: `YOUR_CLIENT_SECRET`
|
||||
- Allowed redirect URLs: `http://localhost:8000/`
|
||||
- Groups claim: `groups` (optional for group based access controll)
|
||||
|
||||
### 2. Setup Kubernetes API Server
|
||||
|
||||
Configure the Kubernetes API server allows your identity provider.
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcClientID: kubernetes
|
||||
oidcGroupsClaim: groups
|
||||
oidcIssuerURL: https://keycloak.example.com/auth/realms/hello
|
||||
```
|
||||
Options:
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
-v, --v int If set to 1 or greater, it shows debug log
|
||||
```
|
||||
|
||||
### 3. Setup kubectl
|
||||
It supports the following keys of `auth-provider` in a kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
|
||||
|
||||
Run the following command to configure `kubectl` to authenticate by your identity provider.
|
||||
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.
|
||||
|
||||
|
||||
### 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
|
||||
kubectl config set-credentials CLUSTER_NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
|
||||
--auth-provider-arg client-id=kubernetes \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
# by the option
|
||||
kubelogin --kubeconfig /path/to/kubeconfig
|
||||
|
||||
# by the environment variable
|
||||
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
|
||||
```
|
||||
|
||||
In actual team operation, you can share the following config to your team members for easy setup.
|
||||
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.
|
||||
|
||||
```yaml
|
||||
#!/bin/sh
|
||||
CLUSTER_NAME="hello.k8s.local"
|
||||
|
||||
# Set the certificate
|
||||
mkdir -p "$HOME/.kube"
|
||||
cat > "$HOME/.kube/$CLUSTER_NAME.crt" <<EOF
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MII...
|
||||
-----END CERTIFICATE-----
|
||||
EOF
|
||||
### Extra scopes
|
||||
|
||||
# Set the cluster
|
||||
kubectl config set-cluster "$CLUSTER_NAME" \
|
||||
--server https://api-xxx.xxx.elb.amazonaws.com \
|
||||
--certificate-authority "$HOME/.kube/$CLUSTER_NAME.crt"
|
||||
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
|
||||
|
||||
# Set the credentials
|
||||
kubectl config set-credentials "$CLUSTER_NAME" \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/hello \
|
||||
--auth-provider-arg client-id=kubernetes \
|
||||
--auth-provider-arg client-secret=YOUR_SECRET
|
||||
|
||||
# Set the context
|
||||
kubectl config set-context "$CLUSTER_NAME" --cluster "$CLUSTER_NAME" --user "$CLUSTER_NAME"
|
||||
|
||||
# Set the current context
|
||||
kubectl config use-context "$CLUSTER_NAME"
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
|
||||
```
|
||||
|
||||
Note that kubectl does not accept multiple scopes and 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
|
||||
```
|
||||
|
||||
|
||||
### Redirect URIs
|
||||
|
||||
By default kubelogin starts the local server at port 8000 or 18000.
|
||||
You need to register the following redirect URIs to the OIDC provider:
|
||||
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if port 8000 is already in use)
|
||||
|
||||
You can change the ports by the option:
|
||||
|
||||
```sh
|
||||
kubelogin --listen-port 12345 --listen-port 23456
|
||||
```
|
||||
|
||||
|
||||
### CA Certificates
|
||||
|
||||
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by kubeconfig or option.
|
||||
|
||||
```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`.
|
||||
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
|
||||
|
||||
|
||||
## Contributions
|
||||
|
||||
This is an open source software licensed under Apache License 2.0.
|
||||
Feel free to open issues and pull requests.
|
||||
|
||||
### Build
|
||||
|
||||
```sh
|
||||
go get github.com/int128/kubelogin
|
||||
```
|
||||
|
||||
### Release
|
||||
|
||||
CircleCI publishes the build to GitHub. See [.circleci/config.yml](.circleci/config.yml).
|
||||
Feel free to open issues and pull requests for improving code and documents.
|
||||
|
||||
100
adaptors/cmd.go
Normal file
100
adaptors/cmd.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
const usage = `Login to the OpenID Connect provider and update the kubeconfig.
|
||||
kubelogin %[2]s
|
||||
|
||||
Examples:
|
||||
# Login to the current provider and update ~/.kube/config
|
||||
%[1]s
|
||||
|
||||
Options:
|
||||
%[3]s
|
||||
Usage:
|
||||
%[1]s [options]`
|
||||
|
||||
var defaultListenPort = []int{8000, 18000}
|
||||
|
||||
func NewCmd(i Cmd) adaptors.Cmd {
|
||||
return &i
|
||||
}
|
||||
|
||||
type Cmd struct {
|
||||
dig.In
|
||||
Login usecases.Login
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
executable := executableName(args[0])
|
||||
f := pflag.NewFlagSet(executable, pflag.ContinueOnError)
|
||||
f.SortFlags = false
|
||||
f.Usage = func() {
|
||||
cmd.Logger.Printf(usage, executable, version, f.FlagUsages())
|
||||
}
|
||||
var o cmdOptions
|
||||
f.StringVar(&o.KubeConfig, "kubeconfig", "", "Path to the kubeconfig file")
|
||||
f.StringVar(&o.KubeContext, "context", "", "The name of the kubeconfig context to use")
|
||||
f.StringVar(&o.KubeUser, "user", "", "The name of the kubeconfig user to use. Prior to --context")
|
||||
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
|
||||
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
|
||||
|
||||
if err := f.Parse(args[1:]); err != nil {
|
||||
if err == pflag.ErrHelp {
|
||||
return 1
|
||||
}
|
||||
cmd.Logger.Printf("Error: invalid arguments: %s", err)
|
||||
return 1
|
||||
}
|
||||
if len(f.Args()) > 0 {
|
||||
cmd.Logger.Printf("Error: too many arguments")
|
||||
return 1
|
||||
}
|
||||
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
in := usecases.LoginIn{
|
||||
KubeConfigFilename: o.KubeConfig,
|
||||
KubeContextName: kubeconfig.ContextName(o.KubeContext),
|
||||
KubeUserName: kubeconfig.UserName(o.KubeUser),
|
||||
CertificateAuthorityFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
}
|
||||
if err := cmd.Login.Do(ctx, in); err != nil {
|
||||
cmd.Logger.Printf("Error: %s", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type cmdOptions struct {
|
||||
KubeConfig string
|
||||
KubeContext string
|
||||
KubeUser string
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
Verbose int
|
||||
}
|
||||
|
||||
func executableName(arg0 string) string {
|
||||
if strings.HasPrefix(arg0, "kubectl-") {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(arg0, "-", " "), "_", "-")
|
||||
}
|
||||
return arg0
|
||||
}
|
||||
111
adaptors/cmd_test.go
Normal file
111
adaptors/cmd_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
)
|
||||
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
const executable = "kubelogin"
|
||||
const version = "HEAD"
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
login := mock_usecases.NewMockLogin(ctrl)
|
||||
login.EXPECT().
|
||||
Do(ctx, usecases.LoginIn{
|
||||
ListenPort: defaultListenPort,
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
Login: login,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
login := mock_usecases.NewMockLogin(ctrl)
|
||||
login.EXPECT().
|
||||
Do(ctx, usecases.LoginIn{
|
||||
KubeConfigFilename: "/path/to/kubeconfig",
|
||||
KubeContextName: "hello.k8s.local",
|
||||
KubeUserName: "google",
|
||||
CertificateAuthorityFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: []int{10080, 20080},
|
||||
SkipOpenBrowser: true,
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
SetLevel(adaptors.LogLevel(1))
|
||||
|
||||
cmd := Cmd{
|
||||
Login: login,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"--kubeconfig", "/path/to/kubeconfig",
|
||||
"--context", "hello.k8s.local",
|
||||
"--user", "google",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cmd := Cmd{
|
||||
Login: mock_usecases.NewMockLogin(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCmd_executableName(t *testing.T) {
|
||||
t.Run("kubelogin", func(t *testing.T) {
|
||||
e := executableName("kubelogin")
|
||||
if e != "kubelogin" {
|
||||
t.Errorf("executableName wants kubelogin but %s", e)
|
||||
}
|
||||
})
|
||||
t.Run("kubectl-oidc_login", func(t *testing.T) {
|
||||
e := executableName("kubectl-oidc_login")
|
||||
if e != "kubectl oidc-login" {
|
||||
t.Errorf("executableName wants kubectl oidc-login but %s", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
85
adaptors/http.go
Normal file
85
adaptors/http.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/infrastructure"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func NewHTTP(i HTTP) adaptors.HTTP {
|
||||
return &i
|
||||
}
|
||||
|
||||
type HTTP struct {
|
||||
dig.In
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
|
||||
pool := x509.NewCertPool()
|
||||
if filename := config.OIDCConfig.IDPCertificateAuthority(); filename != "" {
|
||||
h.Logger.Debugf(1, "Loading the certificate %s", filename)
|
||||
err := appendCertificateFromFile(pool, filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority")
|
||||
}
|
||||
}
|
||||
if data := config.OIDCConfig.IDPCertificateAuthorityData(); data != "" {
|
||||
h.Logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
|
||||
err := appendEncodedCertificate(pool, data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not load the certificate of idp-certificate-authority-data")
|
||||
}
|
||||
}
|
||||
if config.CertificateAuthorityFilename != "" {
|
||||
h.Logger.Debugf(1, "Loading the certificate %s", config.CertificateAuthorityFilename)
|
||||
err := appendCertificateFromFile(pool, config.CertificateAuthorityFilename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not load the certificate")
|
||||
}
|
||||
}
|
||||
|
||||
var tlsConfig tls.Config
|
||||
if len(pool.Subjects()) > 0 {
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
|
||||
return &http.Client{
|
||||
Transport: &infrastructure.LoggingTransport{
|
||||
Base: &http.Transport{
|
||||
TLSClientConfig: &tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Logger: h.Logger,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not read %s", filename)
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return errors.Errorf("could not append certificate from %s", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not decode base64")
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return errors.Errorf("could not append certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
74
adaptors/interfaces/adaptors.go
Normal file
74
adaptors/interfaces/adaptors.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,OIDC,Logger
|
||||
|
||||
type Cmd interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
}
|
||||
|
||||
type KubeConfig interface {
|
||||
LoadByDefaultRules(filename string) (*kubeconfig.Config, error)
|
||||
LoadFromFile(filename string) (*kubeconfig.Config, error)
|
||||
WriteToFile(config *kubeconfig.Config, filename string) error
|
||||
}
|
||||
|
||||
type HTTP interface {
|
||||
NewClient(config HTTPClientConfig) (*http.Client, error)
|
||||
}
|
||||
|
||||
type HTTPClientConfig struct {
|
||||
OIDCConfig kubeconfig.OIDCConfig
|
||||
CertificateAuthorityFilename string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
type OIDC interface {
|
||||
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
|
||||
Verify(ctx context.Context, in OIDCVerifyIn) (*oidc.IDToken, error)
|
||||
}
|
||||
|
||||
type OIDCAuthenticateIn struct {
|
||||
Config kubeconfig.OIDCConfig
|
||||
Client *http.Client // HTTP client for oidc and oauth2
|
||||
LocalServerPort []int // HTTP server port candidates
|
||||
SkipOpenBrowser bool // skip opening browser if true
|
||||
}
|
||||
|
||||
type OIDCAuthenticateCallback struct {
|
||||
ShowLocalServerURL func(url string)
|
||||
}
|
||||
|
||||
type OIDCAuthenticateOut struct {
|
||||
VerifiedIDToken *oidc.IDToken
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
type OIDCVerifyIn struct {
|
||||
Config kubeconfig.OIDCConfig
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Debugf(level LogLevel, format string, v ...interface{})
|
||||
SetLevel(level LogLevel)
|
||||
IsEnabled(level LogLevel) bool
|
||||
}
|
||||
|
||||
// LogLevel represents a log level for debug.
|
||||
//
|
||||
// 0 = None
|
||||
// 1 = Including in/out
|
||||
// 2 = Including transport headers
|
||||
// 3 = Including transport body
|
||||
//
|
||||
type LogLevel int
|
||||
44
adaptors/kubeconfig.go
Normal file
44
adaptors/kubeconfig.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
func NewKubeConfig() adaptors.KubeConfig {
|
||||
return &KubeConfig{}
|
||||
}
|
||||
|
||||
type KubeConfig struct{}
|
||||
|
||||
// LoadByDefaultRules loads the config by the default rules, that is same as kubectl.
|
||||
func (*KubeConfig) LoadByDefaultRules(filename string) (*kubeconfig.Config, error) {
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
rules.ExplicitPath = filename
|
||||
config, err := rules.Load()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read the kubeconfig")
|
||||
}
|
||||
return (*kubeconfig.Config)(config), err
|
||||
}
|
||||
|
||||
// LoadFromFile loads the config from the single file.
|
||||
func (*KubeConfig) LoadFromFile(filename string) (*kubeconfig.Config, error) {
|
||||
config, err := clientcmd.LoadFromFile(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read the kubeconfig from %s", filename)
|
||||
}
|
||||
return (*kubeconfig.Config)(config), err
|
||||
}
|
||||
|
||||
// WriteToFile writes the config to the single file.
|
||||
func (*KubeConfig) WriteToFile(config *kubeconfig.Config, filename string) error {
|
||||
err := clientcmd.WriteToFile(*(*api.Config)(config), filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not write the kubeconfig to %s", filename)
|
||||
}
|
||||
return err
|
||||
}
|
||||
74
adaptors/kubeconfig_test.go
Normal file
74
adaptors/kubeconfig_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKubeConfig_LoadByDefaultRules(t *testing.T) {
|
||||
var adaptor KubeConfig
|
||||
|
||||
t.Run("google.yaml>keycloak.yaml", func(t *testing.T) {
|
||||
setenv(t, "KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
config, err := adaptor.LoadByDefaultRules("")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not load the configs: %s", err)
|
||||
}
|
||||
if w := "google@hello.k8s.local"; w != config.CurrentContext {
|
||||
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
|
||||
}
|
||||
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
|
||||
t.Errorf("Contexts[google@hello.k8s.local] is missing")
|
||||
}
|
||||
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
|
||||
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
|
||||
}
|
||||
if _, ok := config.AuthInfos["google"]; !ok {
|
||||
t.Errorf("AuthInfos[google] is missing")
|
||||
}
|
||||
if _, ok := config.AuthInfos["keycloak"]; !ok {
|
||||
t.Errorf("AuthInfos[keycloak] is missing")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keycloak.yaml>google.yaml", func(t *testing.T) {
|
||||
setenv(t, "KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
config, err := adaptor.LoadByDefaultRules("")
|
||||
if err != nil {
|
||||
t.Fatalf("Could not load the configs: %s", err)
|
||||
}
|
||||
if w := "keycloak@hello.k8s.local"; w != config.CurrentContext {
|
||||
t.Errorf("CurrentContext wants %s but %s", w, config.CurrentContext)
|
||||
}
|
||||
if _, ok := config.Contexts["google@hello.k8s.local"]; !ok {
|
||||
t.Errorf("Contexts[google@hello.k8s.local] is missing")
|
||||
}
|
||||
if _, ok := config.Contexts["keycloak@hello.k8s.local"]; !ok {
|
||||
t.Errorf("Contexts[keycloak@hello.k8s.local] is missing")
|
||||
}
|
||||
if _, ok := config.AuthInfos["google"]; !ok {
|
||||
t.Errorf("AuthInfos[google] is missing")
|
||||
}
|
||||
if _, ok := config.AuthInfos["keycloak"]; !ok {
|
||||
t.Errorf("AuthInfos[keycloak] is missing")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func setenv(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func unsetenv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("Could not unset the env var %s: %s", key, err)
|
||||
}
|
||||
}
|
||||
49
adaptors/logger.go
Normal file
49
adaptors/logger.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
// NewLogger returns a Logger with the standard log.Logger for messages and debug.
|
||||
func NewLogger() adaptors.Logger {
|
||||
return &Logger{
|
||||
stdLogger: log.New(os.Stderr, "", 0),
|
||||
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
|
||||
}
|
||||
}
|
||||
|
||||
// NewLoggerWith returns a Logger with the given standard log.Logger.
|
||||
func NewLoggerWith(l stdLogger) *Logger {
|
||||
return &Logger{
|
||||
stdLogger: l,
|
||||
debugLogger: l,
|
||||
}
|
||||
}
|
||||
|
||||
type stdLogger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger wraps the standard log.Logger and just provides debug level.
|
||||
type Logger struct {
|
||||
stdLogger
|
||||
debugLogger stdLogger
|
||||
level adaptors.LogLevel
|
||||
}
|
||||
|
||||
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
|
||||
if l.IsEnabled(level) {
|
||||
l.debugLogger.Printf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) SetLevel(level adaptors.LogLevel) {
|
||||
l.level = level
|
||||
}
|
||||
|
||||
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
|
||||
return level <= l.level
|
||||
}
|
||||
53
adaptors/logger_test.go
Normal file
53
adaptors/logger_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
type mockStdLogger struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
|
||||
l.count++
|
||||
}
|
||||
|
||||
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 := &mockStdLogger{}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
31
adaptors/mock_adaptors/logger.go
Normal file
31
adaptors/mock_adaptors/logger.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package mock_adaptors
|
||||
|
||||
import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
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...)
|
||||
}
|
||||
236
adaptors/mock_adaptors/mock_adaptors.go
Normal file
236
adaptors/mock_adaptors/mock_adaptors.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,OIDC,Logger)
|
||||
|
||||
// Package mock_adaptors is a generated GoMock package.
|
||||
package mock_adaptors
|
||||
|
||||
import (
|
||||
context "context"
|
||||
go_oidc "github.com/coreos/go-oidc"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
|
||||
kubeconfig "github.com/int128/kubelogin/kubeconfig"
|
||||
http "net/http"
|
||||
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
|
||||
}
|
||||
|
||||
// LoadByDefaultRules mocks base method
|
||||
func (m *MockKubeConfig) LoadByDefaultRules(arg0 string) (*kubeconfig.Config, error) {
|
||||
ret := m.ctrl.Call(m, "LoadByDefaultRules", arg0)
|
||||
ret0, _ := ret[0].(*kubeconfig.Config)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadByDefaultRules indicates an expected call of LoadByDefaultRules
|
||||
func (mr *MockKubeConfigMockRecorder) LoadByDefaultRules(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadByDefaultRules", reflect.TypeOf((*MockKubeConfig)(nil).LoadByDefaultRules), arg0)
|
||||
}
|
||||
|
||||
// LoadFromFile mocks base method
|
||||
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*kubeconfig.Config, error) {
|
||||
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
|
||||
ret0, _ := ret[0].(*kubeconfig.Config)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadFromFile indicates an expected call of LoadFromFile
|
||||
func (mr *MockKubeConfigMockRecorder) LoadFromFile(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadFromFile", reflect.TypeOf((*MockKubeConfig)(nil).LoadFromFile), arg0)
|
||||
}
|
||||
|
||||
// WriteToFile mocks base method
|
||||
func (m *MockKubeConfig) WriteToFile(arg0 *kubeconfig.Config, arg1 string) error {
|
||||
ret := m.ctrl.Call(m, "WriteToFile", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// WriteToFile indicates an expected call of WriteToFile
|
||||
func (mr *MockKubeConfigMockRecorder) WriteToFile(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteToFile", reflect.TypeOf((*MockKubeConfig)(nil).WriteToFile), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockHTTP is a mock of HTTP interface
|
||||
type MockHTTP struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockHTTPMockRecorder
|
||||
}
|
||||
|
||||
// MockHTTPMockRecorder is the mock recorder for MockHTTP
|
||||
type MockHTTPMockRecorder struct {
|
||||
mock *MockHTTP
|
||||
}
|
||||
|
||||
// NewMockHTTP creates a new mock instance
|
||||
func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP {
|
||||
mock := &MockHTTP{ctrl: ctrl}
|
||||
mock.recorder = &MockHTTPMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// NewClient mocks base method
|
||||
func (m *MockHTTP) NewClient(arg0 interfaces.HTTPClientConfig) (*http.Client, error) {
|
||||
ret := m.ctrl.Call(m, "NewClient", arg0)
|
||||
ret0, _ := ret[0].(*http.Client)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// NewClient indicates an expected call of NewClient
|
||||
func (mr *MockHTTPMockRecorder) NewClient(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockHTTP)(nil).NewClient), arg0)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Authenticate mocks base method
|
||||
func (m *MockOIDC) Authenticate(arg0 context.Context, arg1 interfaces.OIDCAuthenticateIn, arg2 interfaces.OIDCAuthenticateCallback) (*interfaces.OIDCAuthenticateOut, error) {
|
||||
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*interfaces.OIDCAuthenticateOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Authenticate indicates an expected call of Authenticate
|
||||
func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Verify mocks base method
|
||||
func (m *MockOIDC) Verify(arg0 context.Context, arg1 interfaces.OIDCVerifyIn) (*go_oidc.IDToken, error) {
|
||||
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
|
||||
ret0, _ := ret[0].(*go_oidc.IDToken)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Verify indicates an expected call of Verify
|
||||
func (mr *MockOIDCMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDC)(nil).Verify), arg0, arg1)
|
||||
}
|
||||
|
||||
// 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 interfaces.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 interfaces.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 interfaces.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)
|
||||
}
|
||||
73
adaptors/oidc.go
Normal file
73
adaptors/oidc.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/oauth2cli"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func NewOIDC() adaptors.OIDC {
|
||||
return &OIDC{}
|
||||
}
|
||||
|
||||
type OIDC struct{}
|
||||
|
||||
func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
if in.Client != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
|
||||
}
|
||||
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
|
||||
}
|
||||
config := oauth2cli.Config{
|
||||
OAuth2Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: in.Config.ClientID(),
|
||||
ClientSecret: in.Config.ClientSecret(),
|
||||
Scopes: append(in.Config.ExtraScopes(), oidc.ScopeOpenID),
|
||||
},
|
||||
LocalServerPort: in.LocalServerPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
|
||||
ShowLocalServerURL: cb.ShowLocalServerURL,
|
||||
}
|
||||
token, err := oauth2cli.GetToken(ctx, config)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not get a token")
|
||||
}
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID()})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, idToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not verify the id_token")
|
||||
}
|
||||
return &adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: verifiedIDToken,
|
||||
IDToken: idToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*OIDC) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*oidc.IDToken, error) {
|
||||
if in.Client != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
|
||||
}
|
||||
provider, err := oidc.NewProvider(ctx, in.Config.IDPIssuerURL())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
|
||||
}
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: in.Config.ClientID()})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, in.Config.IDToken())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not verify the id_token")
|
||||
}
|
||||
return verifiedIDToken, nil
|
||||
}
|
||||
17
adaptors/testdata/kubeconfig.google.yaml
vendored
Normal file
17
adaptors/testdata/kubeconfig.google.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
apiVersion: v1
|
||||
clusters: []
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: google
|
||||
name: google@hello.k8s.local
|
||||
current-context: google@hello.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: google
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: CLIENT_ID.apps.googleusercontent.com
|
||||
name: oidc
|
||||
16
adaptors/testdata/kubeconfig.keycloak.yaml
vendored
Normal file
16
adaptors/testdata/kubeconfig.keycloak.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hello.k8s.local
|
||||
user: keycloak
|
||||
name: keycloak@hello.k8s.local
|
||||
current-context: keycloak@hello.k8s.local
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: kubernetes
|
||||
name: oidc
|
||||
39
adaptors_test/authserver/authserver.go
Normal file
39
adaptors_test/authserver/authserver.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package authserver
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Config represents server configuration.
|
||||
type Config struct {
|
||||
Addr string
|
||||
Issuer string
|
||||
Scope string
|
||||
TLSServerCert string
|
||||
TLSServerKey string
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenKeyPair *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// Start starts a HTTP server.
|
||||
func Start(t *testing.T, c Config) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: c.Addr,
|
||||
Handler: newHandler(t, c),
|
||||
}
|
||||
go func() {
|
||||
var err error
|
||||
if c.TLSServerCert != "" && c.TLSServerKey != "" {
|
||||
err = s.ListenAndServeTLS(c.TLSServerCert, c.TLSServerKey)
|
||||
} else {
|
||||
err = s.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
108
adaptors_test/authserver/handler.go
Normal file
108
adaptors_test/authserver/handler.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package authserver
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
t *testing.T
|
||||
|
||||
discovery *template.Template
|
||||
token *template.Template
|
||||
jwks *template.Template
|
||||
authCode string
|
||||
|
||||
// Template values
|
||||
Issuer string
|
||||
Scope string // Default to openid
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
PrivateKey struct{ N, E string }
|
||||
}
|
||||
|
||||
func newHandler(t *testing.T, c Config) *handler {
|
||||
tpl, err := template.ParseFiles(
|
||||
"authserver/testdata/oidc-discovery.json",
|
||||
"authserver/testdata/oidc-token.json",
|
||||
"authserver/testdata/oidc-jwks.json",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read the templates: %s", err)
|
||||
}
|
||||
h := handler{
|
||||
t: t,
|
||||
discovery: tpl.Lookup("oidc-discovery.json"),
|
||||
token: tpl.Lookup("oidc-token.json"),
|
||||
jwks: tpl.Lookup("oidc-jwks.json"),
|
||||
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
|
||||
Issuer: c.Issuer,
|
||||
Scope: c.Scope,
|
||||
IDToken: c.IDToken,
|
||||
RefreshToken: c.RefreshToken,
|
||||
}
|
||||
if h.Scope == "" {
|
||||
h.Scope = "openid"
|
||||
}
|
||||
if c.IDTokenKeyPair != nil {
|
||||
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
|
||||
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
|
||||
}
|
||||
return &h
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.serveHTTP(w, r); err != nil {
|
||||
h.t.Logf("[auth-server] Error: %s", err)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
m := r.Method
|
||||
p := r.URL.Path
|
||||
h.t.Logf("[auth-server] %s %s", m, r.RequestURI)
|
||||
switch {
|
||||
case m == "GET" && p == "/.well-known/openid-configuration":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := h.discovery.Execute(w, h); err != nil {
|
||||
return errors.Wrapf(err, "could not execute the template")
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/auth":
|
||||
// Authentication Response
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
|
||||
q := r.URL.Query()
|
||||
if h.Scope != q.Get("scope") {
|
||||
return errors.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
|
||||
}
|
||||
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.authCode)
|
||||
http.Redirect(w, r, to, 302)
|
||||
case m == "POST" && p == "/protocol/openid-connect/token":
|
||||
// Token Response
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return errors.Wrapf(err, "could not parse the form")
|
||||
}
|
||||
if h.authCode != r.Form.Get("code") {
|
||||
return errors.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := h.token.Execute(w, h); err != nil {
|
||||
return errors.Wrapf(err, "could not execute the template")
|
||||
}
|
||||
case m == "GET" && p == "/protocol/openid-connect/certs":
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err := h.jwks.Execute(w, h); err != nil {
|
||||
return errors.Wrapf(err, "could not execute the template")
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Not Found", 404)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
85
adaptors_test/authserver/testdata/oidc-discovery.json
vendored
Normal file
85
adaptors_test/authserver/testdata/oidc-discovery.json
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"issuer": "{{ .Issuer }}",
|
||||
"authorization_endpoint": "{{ .Issuer }}/protocol/openid-connect/auth",
|
||||
"token_endpoint": "{{ .Issuer }}/protocol/openid-connect/token",
|
||||
"token_introspection_endpoint": "{{ .Issuer }}/protocol/openid-connect/token/introspect",
|
||||
"userinfo_endpoint": "{{ .Issuer }}/protocol/openid-connect/userinfo",
|
||||
"end_session_endpoint": "{{ .Issuer }}/protocol/openid-connect/logout",
|
||||
"jwks_uri": "{{ .Issuer }}/protocol/openid-connect/certs",
|
||||
"check_session_iframe": "{{ .Issuer }}/protocol/openid-connect/login-status-iframe.html",
|
||||
"grant_types_supported": [
|
||||
"authorization_code",
|
||||
"implicit",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"client_credentials"
|
||||
],
|
||||
"response_types_supported": [
|
||||
"code",
|
||||
"none",
|
||||
"id_token",
|
||||
"token",
|
||||
"id_token token",
|
||||
"code id_token",
|
||||
"code token",
|
||||
"code id_token token"
|
||||
],
|
||||
"subject_types_supported": [
|
||||
"public",
|
||||
"pairwise"
|
||||
],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"userinfo_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"request_object_signing_alg_values_supported": [
|
||||
"none",
|
||||
"RS256"
|
||||
],
|
||||
"response_modes_supported": [
|
||||
"query",
|
||||
"fragment",
|
||||
"form_post"
|
||||
],
|
||||
"registration_endpoint": "{{ .Issuer }}/clients-registrations/openid-connect",
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"private_key_jwt",
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt"
|
||||
],
|
||||
"token_endpoint_auth_signing_alg_values_supported": [
|
||||
"RS256"
|
||||
],
|
||||
"claims_supported": [
|
||||
"sub",
|
||||
"iss",
|
||||
"auth_time",
|
||||
"name",
|
||||
"given_name",
|
||||
"family_name",
|
||||
"preferred_username",
|
||||
"email"
|
||||
],
|
||||
"claim_types_supported": [
|
||||
"normal"
|
||||
],
|
||||
"claims_parameter_supported": false,
|
||||
"scopes_supported": [
|
||||
"openid",
|
||||
"offline_access",
|
||||
"phone",
|
||||
"address",
|
||||
"email",
|
||||
"profile"
|
||||
],
|
||||
"request_parameter_supported": true,
|
||||
"request_uri_parameter_supported": true,
|
||||
"code_challenge_methods_supported": [
|
||||
"plain",
|
||||
"S256"
|
||||
],
|
||||
"tls_client_certificate_bound_access_tokens": true
|
||||
}
|
||||
12
adaptors_test/authserver/testdata/oidc-jwks.json
vendored
Normal file
12
adaptors_test/authserver/testdata/oidc-jwks.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"alg": "RS256",
|
||||
"use": "sig",
|
||||
"kid": "xxx",
|
||||
"n": "{{ .PrivateKey.N }}",
|
||||
"e": "{{ .PrivateKey.E }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
adaptors_test/authserver/testdata/oidc-token.json
vendored
Normal file
7
adaptors_test/authserver/testdata/oidc-token.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": "{{ .RefreshToken }}",
|
||||
"expires_in": 3600,
|
||||
"id_token": "{{ .IDToken }}"
|
||||
}
|
||||
306
adaptors_test/cmd_test.go
Normal file
306
adaptors_test/cmd_test.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package adaptors_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors_test/authserver"
|
||||
"github.com/int128/kubelogin/adaptors_test/keys"
|
||||
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors_test/logger"
|
||||
"github.com/int128/kubelogin/di"
|
||||
)
|
||||
|
||||
// Run the integration tests.
|
||||
// This assumes that port 800x and 900x are available.
|
||||
//
|
||||
// 1. Start the auth server at port 900x.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for port 800x.
|
||||
// 4. Wait for the Cmd.
|
||||
// 5. Shutdown the auth server.
|
||||
//
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "http://localhost:9001")
|
||||
serverConfig := authserver.Config{
|
||||
Addr: "localhost:9001",
|
||||
Issuer: "http://localhost:9001",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer shutdown(t, ctx, server)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverConfig.Issuer,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
startBrowserRequest(t, ctx, &wg, "http://localhost:8001", nil)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8001")
|
||||
wg.Wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("env:KUBECONFIG", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "http://localhost:9002")
|
||||
serverConfig := authserver.Config{
|
||||
Addr: "localhost:9002",
|
||||
Issuer: "http://localhost:9002",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer shutdown(t, ctx, server)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverConfig.Issuer,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
startBrowserRequest(t, ctx, &wg, "http://localhost:8002", nil)
|
||||
runCmd(t, ctx, "--skip-open-browser", "--listen-port", "8002")
|
||||
wg.Wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "http://localhost:9003")
|
||||
serverConfig := authserver.Config{
|
||||
Addr: "localhost:9003",
|
||||
Issuer: "http://localhost:9003",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
Scope: "profile groups openid",
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer shutdown(t, ctx, server)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverConfig.Issuer,
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
startBrowserRequest(t, ctx, &wg, "http://localhost:8003", nil)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8003")
|
||||
wg.Wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "https://localhost:9004")
|
||||
serverConfig := authserver.Config{
|
||||
Addr: "localhost:9004",
|
||||
Issuer: "https://localhost:9004",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
TLSServerCert: keys.TLSServerCert,
|
||||
TLSServerKey: keys.TLSServerKey,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer shutdown(t, ctx, server)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverConfig.Issuer,
|
||||
IDPCertificateAuthority: keys.TLSCACert,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
startBrowserRequest(t, ctx, &wg, "http://localhost:8004", keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8004")
|
||||
wg.Wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACertData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
idToken := newIDToken(t, "https://localhost:9005")
|
||||
serverConfig := authserver.Config{
|
||||
Addr: "localhost:9005",
|
||||
Issuer: "https://localhost:9005",
|
||||
IDToken: idToken,
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
TLSServerCert: keys.TLSServerCert,
|
||||
TLSServerKey: keys.TLSServerKey,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer shutdown(t, ctx, server)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverConfig.Issuer,
|
||||
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
startBrowserRequest(t, ctx, &wg, "http://localhost:8005", keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "8005")
|
||||
wg.Wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
serverConfig := authserver.Config{
|
||||
Addr: "localhost:9006",
|
||||
Issuer: "http://localhost:9006",
|
||||
IDTokenKeyPair: keys.JWSKeyPair,
|
||||
}
|
||||
server := authserver.Start(t, serverConfig)
|
||||
defer shutdown(t, ctx, server)
|
||||
|
||||
idToken := newIDToken(t, serverConfig.Issuer)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverConfig.Issuer,
|
||||
IDToken: idToken,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer string) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
}
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
s, err := token.SignedString(keys.JWSKeyPair)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not sign the claims: %s", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func runCmd(t *testing.T, ctx context.Context, args ...string) {
|
||||
t.Helper()
|
||||
newLogger := func() adaptors.Logger {
|
||||
return logger.New(t)
|
||||
}
|
||||
if err := di.InvokeWithExtra(func(cmd adaptors.Cmd) {
|
||||
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}, newLogger); err != nil {
|
||||
t.Errorf("Invoke returned error: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startBrowserRequest(t *testing.T, ctx context.Context, wg *sync.WaitGroup, url string, tlsConfig *tls.Config) {
|
||||
t.Helper()
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
}
|
||||
|
||||
func shutdown(t *testing.T, ctx context.Context, s *http.Server) {
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
t.Errorf("Could not shutdown the auth server: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setenv(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func unsetenv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("Could not unset the env var %s: %s", key, err)
|
||||
}
|
||||
}
|
||||
71
adaptors_test/keys/keys.go
Normal file
71
adaptors_test/keys/keys.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package keys
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// TLSCACert is path to the CA certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const TLSCACert = "keys/testdata/ca.crt"
|
||||
|
||||
// TLSCACertAsBase64 is a base64 encoded string of TLSCACert.
|
||||
var TLSCACertAsBase64 string
|
||||
|
||||
// TLSCACertAsConfig is a TLS config including TLSCACert.
|
||||
var TLSCACertAsConfig = &tls.Config{RootCAs: x509.NewCertPool()}
|
||||
|
||||
// TLSServerCert is path to the server certificate.
|
||||
// This should be generated by Makefile before test.
|
||||
const TLSServerCert = "keys/testdata/server.crt"
|
||||
|
||||
// TLSServerKey is path to the server key.
|
||||
// This should be generated by Makefile before test.
|
||||
const TLSServerKey = "keys/testdata/server.key"
|
||||
|
||||
// JWSKey is path to the key for signing ID tokens.
|
||||
const JWSKey = "keys/testdata/jws.key"
|
||||
|
||||
// JWSKeyPair is the key pair loaded from JWSKey.
|
||||
var JWSKeyPair *rsa.PrivateKey
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
JWSKeyPair, err = readPrivateKey(JWSKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(TLSCACert)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
TLSCACertAsBase64 = base64.StdEncoding.EncodeToString(b)
|
||||
if !TLSCACertAsConfig.RootCAs.AppendCertsFromPEM(b) {
|
||||
panic("could not append the CA cert")
|
||||
}
|
||||
}
|
||||
|
||||
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not read JWSKey")
|
||||
}
|
||||
block, rest := pem.Decode(b)
|
||||
if block == nil {
|
||||
return nil, errors.New("could not decode PEM")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("PEM should contain single key but multiple keys")
|
||||
}
|
||||
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse the key")
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
4
adaptors_test/keys/testdata/.gitignore
vendored
Normal file
4
adaptors_test/keys/testdata/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/CA
|
||||
*.key
|
||||
*.csr
|
||||
*.crt
|
||||
53
adaptors_test/keys/testdata/Makefile
vendored
Normal file
53
adaptors_test/keys/testdata/Makefile
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: server.crt ca.crt jws.key
|
||||
|
||||
clean:
|
||||
rm -v ca.* server.*
|
||||
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
ca.csr: openssl.cnf ca.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key ca.key \
|
||||
-subj "/CN=Hello CA" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
ca.crt: ca.csr ca.key
|
||||
openssl x509 -req \
|
||||
-signkey ca.key \
|
||||
-in ca.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
server.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -config openssl.cnf \
|
||||
-new \
|
||||
-key server.key \
|
||||
-subj "/CN=localhost" \
|
||||
-out $@
|
||||
openssl req -noout -text -in $@
|
||||
|
||||
server.crt: openssl.cnf server.csr ca.key ca.crt
|
||||
rm -fr ./CA
|
||||
mkdir -p ./CA
|
||||
touch CA/index.txt
|
||||
touch CA/index.txt.attr
|
||||
echo 00 > CA/serial
|
||||
openssl ca -config openssl.cnf \
|
||||
-extensions v3_req \
|
||||
-batch \
|
||||
-cert ca.crt \
|
||||
-keyfile ca.key \
|
||||
-in server.csr \
|
||||
-out $@
|
||||
openssl x509 -text -in $@
|
||||
|
||||
jws.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
37
adaptors_test/keys/testdata/openssl.cnf
vendored
Normal file
37
adaptors_test/keys/testdata/openssl.cnf
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = ./CA
|
||||
certs = $dir
|
||||
crl_dir = $dir
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir
|
||||
default_md = sha256
|
||||
policy = policy_match
|
||||
serial = $dir/serial
|
||||
default_days = 365
|
||||
|
||||
[ policy_match ]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[ req_distinguished_name ]
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = DNS:localhost
|
||||
|
||||
[ v3_ca ]
|
||||
basicConstraints = CA:true
|
||||
79
adaptors_test/kubeconfig/kubeconfig.go
Normal file
79
adaptors_test/kubeconfig/kubeconfig.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Values represents values in .kubeconfig template.
|
||||
type Values struct {
|
||||
Issuer string
|
||||
ExtraScopes string
|
||||
IDPCertificateAuthority string
|
||||
IDPCertificateAuthorityData string
|
||||
IDToken string
|
||||
}
|
||||
|
||||
// Create creates a kubeconfig file and returns path to it.
|
||||
func Create(t *testing.T, v *Values) string {
|
||||
t.Helper()
|
||||
f, err := ioutil.TempFile("", "kubeconfig")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
tpl, err := template.ParseFiles("kubeconfig/testdata/kubeconfig.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tpl.Execute(f, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
type AuthProviderConfig struct {
|
||||
IDToken string `yaml:"id-token"`
|
||||
RefreshToken string `yaml:"refresh-token"`
|
||||
}
|
||||
|
||||
// Verify returns true if the kubeconfig has valid values.
|
||||
func Verify(t *testing.T, kubeconfig string, want AuthProviderConfig) {
|
||||
t.Helper()
|
||||
f, err := os.Open(kubeconfig)
|
||||
if err != nil {
|
||||
t.Errorf("could not open kubeconfig: %s", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var y struct {
|
||||
Users []struct {
|
||||
User struct {
|
||||
AuthProvider struct {
|
||||
Config AuthProviderConfig `yaml:"config"`
|
||||
} `yaml:"auth-provider"`
|
||||
} `yaml:"user"`
|
||||
} `yaml:"users"`
|
||||
}
|
||||
d := yaml.NewDecoder(f)
|
||||
if err := d.Decode(&y); err != nil {
|
||||
t.Errorf("could not decode YAML: %s", err)
|
||||
return
|
||||
}
|
||||
if len(y.Users) != 1 {
|
||||
t.Errorf("len(users) wants 1 but %d", len(y.Users))
|
||||
return
|
||||
}
|
||||
currentConfig := y.Users[0].User.AuthProvider.Config
|
||||
if currentConfig.IDToken != want.IDToken {
|
||||
t.Errorf("id-token wants %s but %s", want.IDToken, currentConfig.IDToken)
|
||||
}
|
||||
if currentConfig.RefreshToken != want.RefreshToken {
|
||||
t.Errorf("refresh-token wants %s but %s", want.RefreshToken, currentConfig.RefreshToken)
|
||||
}
|
||||
}
|
||||
4
adaptors_test/kubeconfig/testdata/dummy.yaml
vendored
Normal file
4
adaptors_test/kubeconfig/testdata/dummy.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
current-context: dummy
|
||||
kind: Config
|
||||
preferences: {}
|
||||
34
adaptors_test/kubeconfig/testdata/kubeconfig.yaml
vendored
Normal file
34
adaptors_test/kubeconfig/testdata/kubeconfig.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
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:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: kubernetes
|
||||
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
|
||||
idp-issuer-url: {{ .Issuer }}
|
||||
#{{ if .ExtraScopes }}
|
||||
extra-scopes: {{ .ExtraScopes }}
|
||||
#{{ end }}
|
||||
#{{ if .IDPCertificateAuthority }}
|
||||
idp-certificate-authority: {{ .IDPCertificateAuthority }}
|
||||
#{{ end }}
|
||||
#{{ if .IDPCertificateAuthorityData }}
|
||||
idp-certificate-authority-data: {{ .IDPCertificateAuthorityData }}
|
||||
#{{ end }}
|
||||
#{{ if .IDToken }}
|
||||
id-token: {{ .IDToken }}
|
||||
#{{ end }}
|
||||
name: oidc
|
||||
21
adaptors_test/logger/logger.go
Normal file
21
adaptors_test/logger/logger.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
func New(t testingLogger) *adaptors.Logger {
|
||||
return adaptors.NewLoggerWith(&bridge{t})
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/authz"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// TokenSet is a set of tokens and claims.
|
||||
type TokenSet struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
Claims *Claims
|
||||
}
|
||||
|
||||
// Claims represents properties in the ID token.
|
||||
type Claims struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// GetTokenSet retrieves a token from the OIDC provider.
|
||||
func GetTokenSet(ctx context.Context, issuer string, clientID string, clientSecret string) (*TokenSet, error) {
|
||||
provider, err := oidc.NewProvider(ctx, issuer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not access OIDC issuer: %s", err)
|
||||
}
|
||||
flow := authz.BrowserAuthCodeFlow{
|
||||
Port: 8000,
|
||||
Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
Scopes: []string{oidc.ScopeOpenID, "email"},
|
||||
},
|
||||
}
|
||||
token, err := flow.GetToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not get a token: %s", err)
|
||||
}
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: clientID})
|
||||
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not verify the id_token: %s", err)
|
||||
}
|
||||
var claims Claims
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("Could not extract claims from the token response: %s", err)
|
||||
}
|
||||
return &TokenSet{
|
||||
IDToken: rawIDToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
Claims: &claims,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Flow represents an authorization method.
|
||||
type Flow interface {
|
||||
GetToken(context.Context) (*oauth2.Token, error)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// BrowserAuthCodeFlow is a flow to get a token by browser interaction.
|
||||
type BrowserAuthCodeFlow struct {
|
||||
oauth2.Config
|
||||
Port int // HTTP server port
|
||||
}
|
||||
|
||||
// GetToken returns a token.
|
||||
func (f *BrowserAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
|
||||
f.Config.RedirectURL = fmt.Sprintf("http://localhost:%d/", f.Port)
|
||||
state, err := generateOAuthState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("Open http://localhost:%d for authorization", f.Port)
|
||||
code, err := f.getCode(ctx, &f.Config, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := f.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (f *BrowserAuthCodeFlow) getCode(ctx context.Context, config *oauth2.Config, state string) (string, error) {
|
||||
codeCh := make(chan string)
|
||||
errCh := make(chan error)
|
||||
server := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", f.Port),
|
||||
Handler: &handler{
|
||||
AuthCodeURL: config.AuthCodeURL(state),
|
||||
Callback: func(code string, actualState string, err error) {
|
||||
switch {
|
||||
case err != nil:
|
||||
errCh <- err
|
||||
case actualState != state:
|
||||
errCh <- fmt.Errorf("OAuth state did not match, should be %s but %s", state, actualState)
|
||||
default:
|
||||
codeCh <- code
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case err := <-errCh:
|
||||
server.Shutdown(ctx)
|
||||
return "", err
|
||||
case code := <-codeCh:
|
||||
server.Shutdown(ctx)
|
||||
return code, nil
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
AuthCodeURL string
|
||||
Callback func(code string, state string, err error)
|
||||
}
|
||||
|
||||
func (s *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("%s %s", r.Method, r.RequestURI)
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
code := r.URL.Query().Get("code")
|
||||
state := r.URL.Query().Get("state")
|
||||
errorCode := r.URL.Query().Get("error")
|
||||
errorDescription := r.URL.Query().Get("error_description")
|
||||
switch {
|
||||
case code != "":
|
||||
s.Callback(code, state, nil)
|
||||
fmt.Fprintf(w, "Back to command line.")
|
||||
case errorCode != "":
|
||||
s.Callback("", "", fmt.Errorf("OAuth Error: %s %s", errorCode, errorDescription))
|
||||
fmt.Fprintf(w, "Back to command line.")
|
||||
default:
|
||||
http.Redirect(w, r, s.AuthCodeURL, 302)
|
||||
}
|
||||
default:
|
||||
http.Error(w, "Not Found", 404)
|
||||
}
|
||||
}
|
||||
35
authz/cli.go
35
authz/cli.go
@@ -1,35 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// CLIAuthCodeFlow is a flow to get a token by keyboard interaction.
|
||||
type CLIAuthCodeFlow struct {
|
||||
oauth2.Config
|
||||
}
|
||||
|
||||
// GetToken returns a token by browser interaction.
|
||||
func (f *CLIAuthCodeFlow) GetToken(ctx context.Context) (*oauth2.Token, error) {
|
||||
f.Config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
|
||||
state, err := generateOAuthState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authCodeURL := f.Config.AuthCodeURL(state)
|
||||
log.Printf("Open %s for authorization", authCodeURL)
|
||||
fmt.Print("Enter code: ")
|
||||
var code string
|
||||
if _, err := fmt.Scanln(&code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := f.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not exchange oauth code: %s", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func generateOAuthState() (string, error) {
|
||||
var n uint64
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", n), nil
|
||||
}
|
||||
42
di/di.go
Normal file
42
di/di.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Package di provides dependency injection.
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
adaptorsInterfaces "github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
var constructors = []interface{}{
|
||||
usecases.NewLogin,
|
||||
|
||||
adaptors.NewCmd,
|
||||
adaptors.NewKubeConfig,
|
||||
adaptors.NewOIDC,
|
||||
adaptors.NewHTTP,
|
||||
}
|
||||
|
||||
var extraConstructors = []interface{}{
|
||||
adaptors.NewLogger,
|
||||
}
|
||||
|
||||
// Invoke runs the function with the default constructors.
|
||||
func Invoke(f func(cmd adaptorsInterfaces.Cmd)) error {
|
||||
return InvokeWithExtra(f, extraConstructors...)
|
||||
}
|
||||
|
||||
// InvokeWithExtra runs the function with the given constructors.
|
||||
func InvokeWithExtra(f func(cmd adaptorsInterfaces.Cmd), extra ...interface{}) error {
|
||||
c := dig.New()
|
||||
for _, constructor := range append(constructors, extra...) {
|
||||
if err := c.Provide(constructor); err != nil {
|
||||
return errors.Wrapf(err, "could not provide the constructor")
|
||||
}
|
||||
}
|
||||
if err := c.Invoke(f); err != nil {
|
||||
return errors.Wrapf(err, "could not invoke")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
18
di/di_test.go
Normal file
18
di/di_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package di_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
adaptors "github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/di"
|
||||
)
|
||||
|
||||
func TestInvoke(t *testing.T) {
|
||||
if err := di.Invoke(func(cmd adaptors.Cmd) {
|
||||
if cmd == nil {
|
||||
t.Errorf("cmd wants non-nil but nil")
|
||||
}
|
||||
}); err != nil {
|
||||
t.Fatalf("Invoke returned error: %+v", err)
|
||||
}
|
||||
}
|
||||
100
docs/google.md
Normal file
100
docs/google.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Getting Started with Google Identity Platform
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have a Google account.
|
||||
- You have the Cluster Admin role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed to your computer.
|
||||
|
||||
## 1. Setup Google API
|
||||
|
||||
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
|
||||
|
||||
- Application Type: Other
|
||||
|
||||
## 2. Setup Kubernetes API server
|
||||
|
||||
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
|
||||
### kops
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://accounts.google.com
|
||||
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
```
|
||||
|
||||
## 3. Setup Kubernetes cluster
|
||||
|
||||
Here assign the `cluster-admin` role to you.
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: oidc-admin-group
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: User
|
||||
name: https://accounts.google.com#1234567890
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 4. Setup kubectl
|
||||
|
||||
Configure `kubectl` for the OIDC authentication.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
|
||||
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubelogin
|
||||
|
||||
Run `kubelogin`.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/08/10 10:36:38 Reading .kubeconfig
|
||||
2018/08/10 10:36:38 Using current context: hello.k8s.local
|
||||
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
|
||||
2018/08/10 10:36:45 GET /
|
||||
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
|
||||
2018/08/10 10:37:08 Updated .kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
idp-issuer-url: https://accounts.google.com
|
||||
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
|
||||
client-secret: YOUR_SECRET
|
||||
id-token: ey... # kubelogin will update ID token here
|
||||
refresh-token: ey... # kubelogin will update refresh token here
|
||||
name: oidc
|
||||
```
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
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
|
||||
```
|
||||
114
docs/keycloak.md
Normal file
114
docs/keycloak.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Getting Started with Keycloak
|
||||
|
||||
## Prerequisite
|
||||
|
||||
- You have an administrator role of the Keycloak realm.
|
||||
- You have an administrator role of the Kubernetes cluster.
|
||||
- You can configure the Kubernetes API server.
|
||||
- `kubectl` and `kubelogin` are installed.
|
||||
|
||||
## 1. Setup Keycloak
|
||||
|
||||
Open the Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Client ID: `kubernetes`
|
||||
- Valid Redirect URLs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
|
||||
You can associate client roles by adding the following mapper:
|
||||
|
||||
- Name: `groups`
|
||||
- Mapper Type: `User Client Role`
|
||||
- Client ID: `kubernetes`
|
||||
- Client Role prefix: `kubernetes:`
|
||||
- Token Claim Name: `groups`
|
||||
- Add to ID token: on
|
||||
|
||||
For example, if you have the `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
|
||||
## 2. Setup Kubernetes API server
|
||||
|
||||
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and add the following spec:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
oidcClientID: kubernetes
|
||||
oidcGroupsClaim: groups
|
||||
```
|
||||
|
||||
## 3. Setup Kubernetes cluster
|
||||
|
||||
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
|
||||
|
||||
```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: Group
|
||||
name: kubernetes:admin
|
||||
```
|
||||
|
||||
You can create a custom role and assign it as well.
|
||||
|
||||
## 4. Setup kubectl
|
||||
|
||||
Configure `kubectl` for the OIDC authentication.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
|
||||
--auth-provider-arg client-id=kubernetes \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubelogin
|
||||
|
||||
Run `kubelogin`.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
2018/08/10 10:36:38 Reading .kubeconfig
|
||||
2018/08/10 10:36:38 Using current context: hello.k8s.local
|
||||
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
|
||||
2018/08/10 10:36:45 GET /
|
||||
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
|
||||
2018/08/10 10:37:08 Updated .kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: hello.k8s.local
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
idp-issuer-url: https://keycloak.example.com/auth/realms/YOUR_REALM
|
||||
client-id: kubernetes
|
||||
client-secret: YOUR_SECRET
|
||||
id-token: ey... # kubelogin will update ID token here
|
||||
refresh-token: ey... # kubelogin will update refresh token here
|
||||
name: oidc
|
||||
```
|
||||
|
||||
Make sure you can access to the Kubernetes cluster.
|
||||
|
||||
```
|
||||
% kubectl get nodes
|
||||
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
|
||||
```
|
||||
40
docs/team_ops.md
Normal file
40
docs/team_ops.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Team Operation
|
||||
|
||||
## 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:
|
||||
auth-provider:
|
||||
name: oidc
|
||||
config:
|
||||
client-id: YOUR_CLIEND_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: YOUR_ISSUER
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for easy onboarding.
|
||||
30
go.mod
Normal file
30
go.mod
Normal file
@@ -0,0 +1,30 @@
|
||||
module github.com/int128/kubelogin
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/gogo/protobuf v1.2.1 // indirect
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/int128/oauth2cli v1.4.0
|
||||
github.com/json-iterator/go v1.1.6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
go.uber.org/dig v1.7.0
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471 // indirect
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 // indirect
|
||||
k8s.io/client-go v10.0.0+incompatible
|
||||
k8s.io/klog v0.2.0 // indirect
|
||||
sigs.k8s.io/yaml v1.1.0 // indirect
|
||||
)
|
||||
82
go.sum
Normal file
82
go.sum
Normal file
@@ -0,0 +1,82 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/int128/oauth2cli v1.4.0 h1:Xt4uk2lIb9Mf9Xyd5o43Hf9iV5izb2jYK3zRX/cPgh0=
|
||||
github.com/int128/oauth2cli v1.4.0/go.mod h1:81pWOyFVt1TRyZ7lZDtZuAGOE/S/jEpb1mpocRopI6U=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
|
||||
go.uber.org/dig v1.7.0/go.mod h1:z+dSd2TP9Usi48jL8M3v63iSBVkiwtVyMKxMZYYauPg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
|
||||
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/client-go v10.0.0+incompatible h1:+xQQxwjrcIPWDMJBAS+1G2FNk1McoPnb53xkvcDiDqE=
|
||||
k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
|
||||
k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
|
||||
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
50
infrastructure/http.go
Normal file
50
infrastructure/http.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelDumpHeaders = 2
|
||||
logLevelDumpBody = 3
|
||||
)
|
||||
|
||||
type LoggingTransport struct {
|
||||
Base http.RoundTripper
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) 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 *LoggingTransport) IsDumpEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpHeaders)
|
||||
}
|
||||
|
||||
func (t *LoggingTransport) IsDumpBodyEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpBody)
|
||||
}
|
||||
90
infrastructure/http_test.go
Normal file
90
infrastructure/http_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package infrastructure
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"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 := &LoggingTransport{
|
||||
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 := &LoggingTransport{
|
||||
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 := &LoggingTransport{
|
||||
Logger: logger,
|
||||
}
|
||||
if transport.IsDumpBodyEnabled() != true {
|
||||
t.Errorf("IsDumpBodyEnabled wants true")
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,56 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FindCurrentAuthInfo returns the authInfo of current context.
|
||||
// If the current context does not exist, this returns nil.
|
||||
func FindCurrentAuthInfo(config *api.Config) *api.AuthInfo {
|
||||
context := config.Contexts[config.CurrentContext]
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
return config.AuthInfos[context.AuthInfo]
|
||||
}
|
||||
|
||||
func ToOIDCAuthProviderConfig(authInfo *api.AuthInfo) (*OIDCAuthProviderConfig, error) {
|
||||
if authInfo.AuthProvider == nil {
|
||||
return nil, fmt.Errorf("auth-provider is not set, did you setup kubectl as listed here: https://github.com/int128/kubelogin#3-setup-kubectl")
|
||||
}
|
||||
if authInfo.AuthProvider.Name != "oidc" {
|
||||
return nil, fmt.Errorf("auth-provider `%s` is not supported", authInfo.AuthProvider.Name)
|
||||
}
|
||||
return (*OIDCAuthProviderConfig)(authInfo.AuthProvider), nil
|
||||
}
|
||||
|
||||
type OIDCAuthProviderConfig api.AuthProviderConfig
|
||||
// OIDCConfig represents config of an oidc auth-provider.
|
||||
type OIDCConfig map[string]string
|
||||
|
||||
// IDPIssuerURL returns the idp-issuer-url.
|
||||
func (c *OIDCAuthProviderConfig) IDPIssuerURL() string {
|
||||
return c.Config["idp-issuer-url"]
|
||||
func (c OIDCConfig) IDPIssuerURL() string {
|
||||
return c["idp-issuer-url"]
|
||||
}
|
||||
|
||||
// ClientID returns the client-id.
|
||||
func (c *OIDCAuthProviderConfig) ClientID() string {
|
||||
return c.Config["client-id"]
|
||||
func (c OIDCConfig) ClientID() string {
|
||||
return c["client-id"]
|
||||
}
|
||||
|
||||
// ClientSecret returns the client-secret.
|
||||
func (c *OIDCAuthProviderConfig) ClientSecret() string {
|
||||
return c.Config["client-secret"]
|
||||
func (c OIDCConfig) ClientSecret() string {
|
||||
return c["client-secret"]
|
||||
}
|
||||
|
||||
func (c *OIDCAuthProviderConfig) SetIDToken(idToken string) {
|
||||
c.Config["id-token"] = idToken
|
||||
// IDPCertificateAuthority returns the idp-certificate-authority.
|
||||
func (c OIDCConfig) IDPCertificateAuthority() string {
|
||||
return c["idp-certificate-authority"]
|
||||
}
|
||||
|
||||
func (c *OIDCAuthProviderConfig) SetRefreshToken(refreshToken string) {
|
||||
c.Config["refresh-token"] = refreshToken
|
||||
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
|
||||
func (c OIDCConfig) IDPCertificateAuthorityData() string {
|
||||
return c["idp-certificate-authority-data"]
|
||||
}
|
||||
|
||||
// ExtraScopes returns the extra-scopes.
|
||||
func (c OIDCConfig) ExtraScopes() []string {
|
||||
if c["extra-scopes"] == "" {
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(c["extra-scopes"], ",")
|
||||
}
|
||||
|
||||
// IDToken returns the id-token.
|
||||
func (c OIDCConfig) IDToken() string {
|
||||
return c["id-token"]
|
||||
}
|
||||
|
||||
// SetIDToken replaces the id-token.
|
||||
func (c OIDCConfig) SetIDToken(idToken string) {
|
||||
c["id-token"] = idToken
|
||||
}
|
||||
|
||||
// SetRefreshToken replaces the refresh-token.
|
||||
func (c OIDCConfig) SetRefreshToken(refreshToken string) {
|
||||
c["refresh-token"] = refreshToken
|
||||
}
|
||||
|
||||
@@ -1,48 +1,66 @@
|
||||
// Package kubeconfig provides the models of kubeconfig file.
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
const userKubeConfig = "~/.kube/config"
|
||||
type ContextName string
|
||||
type UserName string
|
||||
|
||||
// Find returns path to the kubeconfig file,
|
||||
// that is given by env:KUBECONFIG or ~/.kube/config.
|
||||
// This returns an error if it is not found or I/O error occurred.
|
||||
func Find() (string, error) {
|
||||
path := os.Getenv("KUBECONFIG")
|
||||
if path == "" {
|
||||
var err error
|
||||
path, err = homedir.Expand(userKubeConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not expand %s: %s", userKubeConfig, err)
|
||||
// Config represents a config.
|
||||
type Config api.Config
|
||||
|
||||
// Context represents a context.
|
||||
type Context api.Context
|
||||
|
||||
// User represents a user.
|
||||
type User api.AuthInfo
|
||||
|
||||
// CurrentAuth represents the current authentication, that is,
|
||||
// context, user and auth-provider.
|
||||
type CurrentAuth struct {
|
||||
ContextName ContextName // empty if UserName is given
|
||||
Context *Context // nil if UserName is given
|
||||
UserName UserName
|
||||
User *User
|
||||
OIDCConfig OIDCConfig
|
||||
}
|
||||
|
||||
// FindCurrentAuth resolves the current context and user.
|
||||
// If contextName is given, this returns the user of the context.
|
||||
// If userName is given, this ignores the context and returns the user.
|
||||
// If any context or user is not found, this returns an error.
|
||||
func FindCurrentAuth(config *Config, contextName ContextName, userName UserName) (*CurrentAuth, error) {
|
||||
var kubeContext *Context
|
||||
if userName == "" {
|
||||
if contextName == "" {
|
||||
contextName = ContextName(config.CurrentContext)
|
||||
}
|
||||
contextNode := config.Contexts[string(contextName)]
|
||||
if contextNode == nil {
|
||||
return nil, errors.Errorf("context %s does not exist", contextName)
|
||||
}
|
||||
kubeContext = (*Context)(contextNode)
|
||||
userName = UserName(kubeContext.AuthInfo)
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Could not stat %s: %s", userKubeConfig, err)
|
||||
userNode := config.AuthInfos[string(userName)]
|
||||
if userNode == nil {
|
||||
return nil, errors.Errorf("user %s does not exist", userName)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return "", fmt.Errorf("%s should be a file", userKubeConfig)
|
||||
user := (*User)(userNode)
|
||||
if user.AuthProvider == nil {
|
||||
return nil, errors.Errorf("auth-provider is missing")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Load loads the file and returns the Config.
|
||||
func Load(path string) (*api.Config, error) {
|
||||
config, err := clientcmd.LoadFromFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not load kubeconfig from %s: %s", path, err)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// Write writes the config to the file.
|
||||
func Write(config *api.Config, path string) error {
|
||||
return clientcmd.WriteToFile(*config, path)
|
||||
if user.AuthProvider.Name != "oidc" {
|
||||
return nil, errors.Errorf("auth-provider must be oidc but is %s", user.AuthProvider.Name)
|
||||
}
|
||||
return &CurrentAuth{
|
||||
ContextName: contextName,
|
||||
Context: kubeContext,
|
||||
UserName: userName,
|
||||
User: user,
|
||||
OIDCConfig: user.AuthProvider.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
15
kubelogin.rb
Normal file
15
kubelogin.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class Kubelogin < Formula
|
||||
desc "A kubectl plugin for Kubernetes OpenID Connect authentication"
|
||||
homepage "https://github.com/int128/kubelogin"
|
||||
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
|
||||
version "{{ env "VERSION" }}"
|
||||
sha256 "{{ sha256 .darwin_amd64_archive }}"
|
||||
def install
|
||||
bin.install "kubelogin" => "kubelogin"
|
||||
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
|
||||
end
|
||||
test do
|
||||
system "#{bin}/kubelogin -h"
|
||||
system "#{bin}/kubectl-oidc_login -h"
|
||||
end
|
||||
end
|
||||
39
main.go
39
main.go
@@ -3,39 +3,18 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/authn"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/di"
|
||||
)
|
||||
|
||||
var version = "HEAD"
|
||||
|
||||
func main() {
|
||||
path, err := kubeconfig.Find()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not find kubeconfig: %s", err)
|
||||
if err := di.Invoke(func(cmd adaptors.Cmd) {
|
||||
os.Exit(cmd.Run(context.Background(), os.Args, version))
|
||||
}); err != nil {
|
||||
log.Fatalf("Error: %s", err)
|
||||
}
|
||||
log.Printf("Reading %s", path)
|
||||
cfg, err := kubeconfig.Load(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not load kubeconfig: %s", err)
|
||||
}
|
||||
log.Printf("Using current context: %s", cfg.CurrentContext)
|
||||
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
|
||||
if authInfo == nil {
|
||||
log.Fatalf("Could not find current context: %s", cfg.CurrentContext)
|
||||
}
|
||||
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not find auth-provider: %s", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
|
||||
if err != nil {
|
||||
log.Fatalf("Authentication error: %s", err)
|
||||
}
|
||||
|
||||
authProvider.SetIDToken(token.IDToken)
|
||||
authProvider.SetRefreshToken(token.RefreshToken)
|
||||
kubeconfig.Write(cfg, path)
|
||||
log.Printf("Updated %s", path)
|
||||
}
|
||||
|
||||
57
oidc-login.yaml
Normal file
57
oidc-login.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
apiVersion: krew.googlecontainertools.github.com/v1alpha2
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: oidc-login
|
||||
spec:
|
||||
shortDescription: Login for OpenID Connect authentication
|
||||
description: |
|
||||
This plugin gets a token from the OIDC provider and writes it to the kubeconfig.
|
||||
|
||||
Just run:
|
||||
% kubectl oidc-login
|
||||
|
||||
It opens the browser and you can log in to the provider.
|
||||
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
|
||||
|
||||
caveats: |
|
||||
You need to setup the following components:
|
||||
* OIDC provider
|
||||
* Kubernetes API server
|
||||
* Role for your group or user
|
||||
* kubectl authentication
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
homepage: https://github.com/int128/kubelogin
|
||||
version: {{ env "VERSION" }}
|
||||
platforms:
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
|
||||
sha256: "{{ sha256 .linux_amd64_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: "kubelogin"
|
||||
to: "."
|
||||
selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: amd64
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip
|
||||
sha256: "{{ sha256 .darwin_amd64_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: "kubelogin"
|
||||
to: "."
|
||||
selector:
|
||||
matchLabels:
|
||||
os: darwin
|
||||
arch: amd64
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_windows_amd64.zip
|
||||
sha256: "{{ sha256 .windows_amd64_archive }}"
|
||||
bin: kubelogin.exe
|
||||
files:
|
||||
- from: "kubelogin.exe"
|
||||
to: "."
|
||||
selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
23
usecases/interfaces/usecases.go
Normal file
23
usecases/interfaces/usecases.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -package mock_usecases -destination ../mock_usecases/mock_usecases.go github.com/int128/kubelogin/usecases/interfaces Login
|
||||
|
||||
type Login interface {
|
||||
Do(ctx context.Context, in LoginIn) error
|
||||
}
|
||||
|
||||
type LoginIn struct {
|
||||
KubeConfigFilename string // Default to the environment variable or global config as kubectl
|
||||
KubeContextName kubeconfig.ContextName // Default to the current context but ignored if KubeUserName is set
|
||||
KubeUserName kubeconfig.UserName // Default to the user of the context
|
||||
CertificateAuthorityFilename string // Optional
|
||||
SkipTLSVerify bool
|
||||
SkipOpenBrowser bool
|
||||
ListenPort []int
|
||||
}
|
||||
123
usecases/login.go
Normal file
123
usecases/login.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
const oidcConfigErrorMessage = `No OIDC configuration found. Did you setup kubectl for OIDC authentication?
|
||||
kubectl config set-credentials CONTEXT_NAME \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://issuer.example.com \
|
||||
--auth-provider-arg client-id=YOUR_CLIENT_ID \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET`
|
||||
|
||||
func NewLogin(i Login) usecases.Login {
|
||||
return &i
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
dig.In
|
||||
KubeConfig adaptors.KubeConfig
|
||||
HTTP adaptors.HTTP
|
||||
OIDC adaptors.OIDC
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (u *Login) Do(ctx context.Context, in usecases.LoginIn) error {
|
||||
u.Logger.Debugf(1, "WARNING: log may contain your secrets such as token or password")
|
||||
|
||||
mergedKubeConfig, err := u.KubeConfig.LoadByDefaultRules(in.KubeConfigFilename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not load the kubeconfig")
|
||||
}
|
||||
auth, err := kubeconfig.FindCurrentAuth(mergedKubeConfig, in.KubeContextName, in.KubeUserName)
|
||||
if err != nil {
|
||||
u.Logger.Printf(oidcConfigErrorMessage)
|
||||
return errors.Wrapf(err, "could not find the current authentication provider")
|
||||
}
|
||||
u.Logger.Debugf(1, "Using the authentication provider of the user %s", auth.UserName)
|
||||
destinationKubeConfigFilename := auth.User.LocationOfOrigin
|
||||
if destinationKubeConfigFilename == "" {
|
||||
return errors.Errorf("could not determine the kubeconfig to write")
|
||||
}
|
||||
u.Logger.Debugf(1, "A token will be written to %s", destinationKubeConfigFilename)
|
||||
|
||||
hc, err := u.HTTP.NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: auth.OIDCConfig,
|
||||
CertificateAuthorityFilename: in.CertificateAuthorityFilename,
|
||||
SkipTLSVerify: in.SkipTLSVerify,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not set up a HTTP client")
|
||||
}
|
||||
|
||||
if auth.OIDCConfig.IDToken() != "" {
|
||||
u.Logger.Debugf(1, "Found the ID token in the kubeconfig")
|
||||
token, err := u.OIDC.Verify(ctx, adaptors.OIDCVerifyIn{Config: auth.OIDCConfig, Client: hc})
|
||||
if err == nil {
|
||||
u.Logger.Printf("You already have a valid token until %s", token.Expiry)
|
||||
u.dumpIDToken(token)
|
||||
return nil
|
||||
}
|
||||
u.Logger.Debugf(1, "The ID token was invalid: %s", err)
|
||||
}
|
||||
|
||||
out, err := u.OIDC.Authenticate(ctx,
|
||||
adaptors.OIDCAuthenticateIn{
|
||||
Config: auth.OIDCConfig,
|
||||
Client: hc,
|
||||
LocalServerPort: in.ListenPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
},
|
||||
adaptors.OIDCAuthenticateCallback{
|
||||
ShowLocalServerURL: func(url string) {
|
||||
u.Logger.Printf("Open %s for authentication", url)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not get a token from the OIDC provider")
|
||||
}
|
||||
u.Logger.Printf("You got a valid token until %s", out.VerifiedIDToken.Expiry)
|
||||
u.dumpIDToken(out.VerifiedIDToken)
|
||||
|
||||
if err := u.writeToken(destinationKubeConfigFilename, auth.UserName, out); err != nil {
|
||||
return errors.Wrapf(err, "could not write the token to the kubeconfig")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Login) dumpIDToken(token *oidc.IDToken) {
|
||||
var claims map[string]interface{}
|
||||
if err := token.Claims(&claims); err != nil {
|
||||
u.Logger.Debugf(1, "Error while inspection of the ID token: %s", err)
|
||||
}
|
||||
for k, v := range claims {
|
||||
u.Logger.Debugf(1, "The ID token has the claim: %s=%v", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Login) writeToken(filename string, userName kubeconfig.UserName, out *adaptors.OIDCAuthenticateOut) error {
|
||||
config, err := u.KubeConfig.LoadFromFile(filename)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not load %s", filename)
|
||||
}
|
||||
auth, err := kubeconfig.FindCurrentAuth(config, "", userName)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "could not find the user %s in %s", userName, filename)
|
||||
}
|
||||
auth.OIDCConfig.SetIDToken(out.IDToken)
|
||||
auth.OIDCConfig.SetRefreshToken(out.RefreshToken)
|
||||
u.Logger.Debugf(1, "Writing the ID token and refresh token to %s", filename)
|
||||
if err := u.KubeConfig.WriteToFile(config, filename); err != nil {
|
||||
return errors.Wrapf(err, "could not update %s", filename)
|
||||
}
|
||||
u.Logger.Printf("Updated %s", filename)
|
||||
return nil
|
||||
}
|
||||
586
usecases/login_test.go
Normal file
586
usecases/login_test.go
Normal file
@@ -0,0 +1,586 @@
|
||||
package usecases
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors/interfaces"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases/interfaces"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
type loginTestFixture struct {
|
||||
googleOIDCConfig kubeconfig.OIDCConfig
|
||||
googleOIDCConfigWithToken kubeconfig.OIDCConfig
|
||||
googleKubeConfig *kubeconfig.Config
|
||||
googleKubeConfigWithToken *kubeconfig.Config
|
||||
keycloakOIDCConfig kubeconfig.OIDCConfig
|
||||
keycloakOIDCConfigWithToken kubeconfig.OIDCConfig
|
||||
keycloakKubeConfig *kubeconfig.Config
|
||||
keycloakKubeConfigWithToken *kubeconfig.Config
|
||||
mergedKubeConfig *kubeconfig.Config
|
||||
}
|
||||
|
||||
func newLoginTestFixture() loginTestFixture {
|
||||
var f loginTestFixture
|
||||
f.googleOIDCConfig = kubeconfig.OIDCConfig{
|
||||
"client-id": "GOOGLE_CLIENT_ID",
|
||||
"client-secret": "GOOGLE_CLIENT_SECRET",
|
||||
"idp-issuer-url": "https://accounts.google.com",
|
||||
}
|
||||
f.googleKubeConfig = &kubeconfig.Config{
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "googleContext",
|
||||
Contexts: map[string]*api.Context{
|
||||
"googleContext": {
|
||||
LocationOfOrigin: "/path/to/google",
|
||||
AuthInfo: "google",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"google": {
|
||||
LocationOfOrigin: "/path/to/google",
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: f.googleOIDCConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
f.googleOIDCConfigWithToken = kubeconfig.OIDCConfig{
|
||||
"client-id": "GOOGLE_CLIENT_ID",
|
||||
"client-secret": "GOOGLE_CLIENT_SECRET",
|
||||
"idp-issuer-url": "https://accounts.google.com",
|
||||
"id-token": "YOUR_ID_TOKEN",
|
||||
"refresh-token": "YOUR_REFRESH_TOKEN",
|
||||
}
|
||||
f.googleKubeConfigWithToken = &kubeconfig.Config{
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "googleContext",
|
||||
Contexts: map[string]*api.Context{
|
||||
"googleContext": {
|
||||
LocationOfOrigin: "/path/to/google",
|
||||
AuthInfo: "google",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"google": {
|
||||
LocationOfOrigin: "/path/to/google",
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: f.googleOIDCConfigWithToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
f.keycloakOIDCConfig = kubeconfig.OIDCConfig{
|
||||
"client-id": "KEYCLOAK_CLIENT_ID",
|
||||
"client-secret": "KEYCLOAK_CLIENT_SECRET",
|
||||
"idp-issuer-url": "https://keycloak.example.com",
|
||||
}
|
||||
f.keycloakKubeConfig = &kubeconfig.Config{
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "googleContext",
|
||||
Contexts: map[string]*api.Context{
|
||||
"keycloakContext": {
|
||||
LocationOfOrigin: "/path/to/keycloak",
|
||||
AuthInfo: "keycloak",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"keycloak": {
|
||||
LocationOfOrigin: "/path/to/keycloak",
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: f.keycloakOIDCConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
f.keycloakOIDCConfigWithToken = kubeconfig.OIDCConfig{
|
||||
"client-id": "KEYCLOAK_CLIENT_ID",
|
||||
"client-secret": "KEYCLOAK_CLIENT_SECRET",
|
||||
"idp-issuer-url": "https://keycloak.example.com",
|
||||
"id-token": "YOUR_ID_TOKEN",
|
||||
"refresh-token": "YOUR_REFRESH_TOKEN",
|
||||
}
|
||||
f.keycloakKubeConfigWithToken = &kubeconfig.Config{
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "googleContext",
|
||||
Contexts: map[string]*api.Context{
|
||||
"keycloakContext": {
|
||||
LocationOfOrigin: "/path/to/keycloak",
|
||||
AuthInfo: "keycloak",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"keycloak": {
|
||||
LocationOfOrigin: "/path/to/keycloak",
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: f.keycloakOIDCConfigWithToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
f.mergedKubeConfig = &kubeconfig.Config{
|
||||
APIVersion: "v1",
|
||||
CurrentContext: "googleContext",
|
||||
Contexts: map[string]*api.Context{
|
||||
"googleContext": {
|
||||
LocationOfOrigin: "/path/to/google",
|
||||
AuthInfo: "google",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
"keycloakContext": {
|
||||
LocationOfOrigin: "/path/to/keycloak",
|
||||
AuthInfo: "keycloak",
|
||||
Cluster: "example.k8s.local",
|
||||
},
|
||||
},
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"google": {
|
||||
LocationOfOrigin: "/path/to/google",
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: f.googleOIDCConfig,
|
||||
},
|
||||
},
|
||||
"keycloak": {
|
||||
LocationOfOrigin: "/path/to/keycloak",
|
||||
AuthProvider: &api.AuthProviderConfig{
|
||||
Name: "oidc",
|
||||
Config: f.keycloakOIDCConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func TestLogin_Do(t *testing.T) {
|
||||
httpClient := &http.Client{}
|
||||
|
||||
newMockOIDC := func(ctrl *gomock.Controller, ctx context.Context, in adaptors.OIDCAuthenticateIn) *mock_adaptors.MockOIDC {
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Authenticate(ctx, in, gomock.Any()).
|
||||
Do(func(_ context.Context, _ adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) {
|
||||
cb.ShowLocalServerURL("http://localhost:10000")
|
||||
}).
|
||||
Return(&adaptors.OIDCAuthenticateOut{
|
||||
VerifiedIDToken: &oidc.IDToken{Subject: "SUBJECT"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}, nil)
|
||||
return mockOIDC
|
||||
}
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/google").
|
||||
Return(f.googleKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
ListenPort: []int{10000},
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfigFilename", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("/path/to/kubeconfig").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/google").
|
||||
Return(f.googleKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeConfigFilename: "/path/to/kubeconfig",
|
||||
ListenPort: []int{10000},
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeContextName", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.keycloakOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/keycloak").
|
||||
Return(f.keycloakKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.keycloakKubeConfigWithToken, "/path/to/keycloak")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.keycloakOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeContextName: "keycloakContext",
|
||||
ListenPort: []int{10000},
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeUserName", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.keycloakOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/keycloak").
|
||||
Return(f.keycloakKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.keycloakKubeConfigWithToken, "/path/to/keycloak")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.keycloakOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
KubeUserName: "keycloak",
|
||||
ListenPort: []int{10000},
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SkipTLSVerify", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
SkipTLSVerify: true,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/google").
|
||||
Return(f.googleKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
ListenPort: []int{10000},
|
||||
SkipTLSVerify: true,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SkipOpenBrowser", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/google").
|
||||
Return(f.googleKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
SkipOpenBrowser: true,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
ListenPort: []int{10000},
|
||||
SkipOpenBrowser: true,
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/ValidToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
f.googleOIDCConfig.SetIDToken("VALID_TOKEN")
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
|
||||
mockOIDC := mock_adaptors.NewMockOIDC(ctrl)
|
||||
mockOIDC.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
Client: httpClient,
|
||||
}).
|
||||
Return(&oidc.IDToken{}, nil)
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
ListenPort: []int{10000},
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KubeConfig/InvalidToken", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
f.googleOIDCConfig.SetIDToken("EXPIRED_TOKEN")
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/google").
|
||||
Return(f.googleKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
|
||||
|
||||
mockOIDC := newMockOIDC(ctrl, ctx, adaptors.OIDCAuthenticateIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
})
|
||||
mockOIDC.EXPECT().
|
||||
Verify(ctx, adaptors.OIDCVerifyIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
Client: httpClient,
|
||||
}).
|
||||
Return(nil, errors.New("token is expired"))
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: mockOIDC,
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
ListenPort: []int{10000},
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Certificates", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
f := newLoginTestFixture()
|
||||
f.googleOIDCConfig["idp-certificate-authority"] = "/path/to/cert2"
|
||||
f.googleOIDCConfig["idp-certificate-authority-data"] = "base64encoded"
|
||||
f.googleOIDCConfigWithToken["idp-certificate-authority"] = "/path/to/cert2"
|
||||
f.googleOIDCConfigWithToken["idp-certificate-authority-data"] = "base64encoded"
|
||||
|
||||
mockHTTP := mock_adaptors.NewMockHTTP(ctrl)
|
||||
mockHTTP.EXPECT().
|
||||
NewClient(adaptors.HTTPClientConfig{
|
||||
OIDCConfig: f.googleOIDCConfig,
|
||||
CertificateAuthorityFilename: "/path/to/cert1",
|
||||
}).
|
||||
Return(httpClient, nil)
|
||||
|
||||
mockKubeConfig := mock_adaptors.NewMockKubeConfig(ctrl)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadByDefaultRules("").
|
||||
Return(f.mergedKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
LoadFromFile("/path/to/google").
|
||||
Return(f.googleKubeConfig, nil)
|
||||
mockKubeConfig.EXPECT().
|
||||
WriteToFile(f.googleKubeConfigWithToken, "/path/to/google")
|
||||
|
||||
oidcIn := adaptors.OIDCAuthenticateIn{
|
||||
Config: f.googleOIDCConfig,
|
||||
LocalServerPort: []int{10000},
|
||||
Client: httpClient,
|
||||
}
|
||||
|
||||
u := Login{
|
||||
KubeConfig: mockKubeConfig,
|
||||
HTTP: mockHTTP,
|
||||
OIDC: newMockOIDC(ctrl, ctx, oidcIn),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
if err := u.Do(ctx, usecases.LoginIn{
|
||||
ListenPort: []int{10000},
|
||||
CertificateAuthorityFilename: "/path/to/cert1",
|
||||
}); err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
47
usecases/mock_usecases/mock_usecases.go
Normal file
47
usecases/mock_usecases/mock_usecases.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/usecases/interfaces (interfaces: Login)
|
||||
|
||||
// Package mock_usecases is a generated GoMock package.
|
||||
package mock_usecases
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
interfaces "github.com/int128/kubelogin/usecases/interfaces"
|
||||
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 interfaces.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)
|
||||
}
|
||||
Reference in New Issue
Block a user