mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-25 22:43:48 +00:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ffc914d0e | ||
|
|
fa008bef55 | ||
|
|
50047417ab | ||
|
|
4c0ebb0284 | ||
|
|
c1173accd3 | ||
|
|
d4addcfc6e | ||
|
|
695347e419 | ||
|
|
d5c738697f | ||
|
|
777a60c96c | ||
|
|
6f6e0723f1 | ||
|
|
3fb074a4a8 | ||
|
|
59b5f1bd89 | ||
|
|
70819843f0 | ||
|
|
87c46a24ae | ||
|
|
ac504150b2 | ||
|
|
caeb55f21d | ||
|
|
2ccda6099f | ||
|
|
7f4f72c9e0 | ||
|
|
bfc1568057 | ||
|
|
8758d55bb3 | ||
|
|
d9be392f5a | ||
|
|
af840a519c | ||
|
|
285b3b15a8 | ||
|
|
123d7c8124 | ||
|
|
e2a6b5d4e2 | ||
|
|
ce93c739f8 | ||
|
|
dc646c88f9 | ||
|
|
07e34d2222 | ||
|
|
0e2d402c40 | ||
|
|
db7c260f9d | ||
|
|
a95dc5c794 | ||
|
|
846804f611 | ||
|
|
8b9e31b4c5 | ||
|
|
bc99af6eab | ||
|
|
e3bec130f4 | ||
|
|
d59e3355fe | ||
|
|
9d2d0109d5 | ||
|
|
aac8780caf | ||
|
|
f89525b184 | ||
|
|
a46dab3dfd | ||
|
|
42879dc915 | ||
|
|
7ce98c7119 | ||
|
|
8b5e87de75 | ||
|
|
1b545e1c58 | ||
|
|
2fa306c348 | ||
|
|
9018cd65c5 | ||
|
|
9fe6a09943 | ||
|
|
c53d415255 | ||
|
|
aa0718df16 | ||
|
|
40698536b0 | ||
|
|
7ec53a5dd1 | ||
|
|
3a28e44556 | ||
|
|
f8cca818af | ||
|
|
0c6ca03eb9 | ||
|
|
fe2fbcbc53 | ||
|
|
812a965739 | ||
|
|
6de1fca64c | ||
|
|
0eb8cdc95f | ||
|
|
995c0997d5 | ||
|
|
18b2437819 | ||
|
|
5d5a33b8ea | ||
|
|
a614943642 | ||
|
|
d223175b92 | ||
|
|
6075c9dbe7 | ||
|
|
be43c2ab82 | ||
|
|
512df0c4e4 | ||
|
|
5d5292637f | ||
|
|
76f61300d6 | ||
|
|
f7f1985a89 | ||
|
|
3d47c88a8d | ||
|
|
c7ea97ff23 | ||
|
|
af18e734ea | ||
|
|
b5ae469b41 | ||
|
|
94f480fdc9 | ||
|
|
7acb6e3a7b | ||
|
|
29e9c39a41 | ||
|
|
dd86168e4b | ||
|
|
1d48eab6b3 | ||
|
|
1e655a14b8 | ||
|
|
8a4d1f5169 | ||
|
|
6f417cd30c | ||
|
|
7ba08f4254 | ||
|
|
e778bbdadc | ||
|
|
74108adf00 | ||
|
|
0257b24156 | ||
|
|
b8c29985e7 | ||
|
|
4683a005c7 | ||
|
|
cc48fb4cf7 | ||
|
|
ec7f7a062a | ||
|
|
e9ae98dfaf | ||
|
|
0c582e97ad | ||
|
|
5a71247214 | ||
|
|
4a084756c3 | ||
|
|
cf4e310b2e | ||
|
|
4007e7f61a | ||
|
|
2700e439b9 | ||
|
|
dbf6238029 | ||
|
|
93e893bc36 | ||
|
|
5dc06ae574 | ||
|
|
ab1023757b | ||
|
|
e26dbd118e | ||
|
|
fccef52a73 | ||
|
|
581284c626 | ||
|
|
b5922f9419 | ||
|
|
7a0ca206d1 | ||
|
|
0bca9ef54b | ||
|
|
2fb551bf1b | ||
|
|
0bc117ddc7 | ||
|
|
8c640f6c73 | ||
|
|
8a5efac337 | ||
|
|
d6e0c761ac | ||
|
|
8925226afe | ||
|
|
89a0f9a79e | ||
|
|
74bb4c62c5 | ||
|
|
25c7c1e703 | ||
|
|
6b1e11f071 | ||
|
|
554daf7655 | ||
|
|
d67d16b333 | ||
|
|
3d0973054b | ||
|
|
bf02210f2a | ||
|
|
53e8284b63 | ||
|
|
d9b8d99fae | ||
|
|
3e30346c9b | ||
|
|
1e80481145 | ||
|
|
9242b1917b | ||
|
|
306bf09485 | ||
|
|
4ad77cd5f8 | ||
|
|
c8967faf6b | ||
|
|
315d6151d7 | ||
|
|
1ff03fdfb3 | ||
|
|
5e0fc7f399 | ||
|
|
9423a65f46 | ||
|
|
45417a18fd | ||
|
|
760416fd04 | ||
|
|
0a4ebb26c2 | ||
|
|
de9f7a2a01 | ||
|
|
0006cdda2d | ||
|
|
c89a8a1823 | ||
|
|
4f566a7b32 | ||
|
|
5158159bdd | ||
|
|
3a2aa0c6c0 | ||
|
|
56b17efae1 | ||
|
|
3e5be43d8a | ||
|
|
1ffa927432 | ||
|
|
5c6b461f37 | ||
|
|
6f96ccae62 | ||
|
|
dc88948d88 | ||
|
|
31609a3ed3 | ||
|
|
9685507d7d | ||
|
|
1c66099496 | ||
|
|
681faa07ca |
@@ -2,31 +2,30 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.3
|
||||
- image: cimg/go:1.14
|
||||
steps:
|
||||
- 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
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
|
||||
- checkout
|
||||
# workaround for https://github.com/golang/go/issues/27925
|
||||
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-sum-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
command: go get -v github.com/int128/goxzst github.com/int128/ghcp
|
||||
working_directory: .circleci
|
||||
- run: make check
|
||||
- run: bash <(curl -s https://codecov.io/bash)
|
||||
- run: make run
|
||||
- run: make dist
|
||||
- run: |
|
||||
if [ "$CIRCLE_TAG" ]; then
|
||||
make release
|
||||
fi
|
||||
- save_cache:
|
||||
key: go-sum-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- ~/go/pkg
|
||||
- store_artifacts:
|
||||
path: gotest.log
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
3
.circleci/go.mod
Normal file
3
.circleci/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/int128/kubelogin/.circleci
|
||||
|
||||
go 1.13
|
||||
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [int128] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
30
.github/workflows/acceptance-test.yaml
vendored
Normal file
30
.github/workflows/acceptance-test.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: acceptance-test
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
name: test
|
||||
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.1
|
||||
id: go
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
go-
|
||||
# https://kind.sigs.k8s.io/docs/user/quick-start/
|
||||
- run: |
|
||||
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.7.0/kind-linux-amd64"
|
||||
chmod +x ./kind
|
||||
sudo mv ./kind /usr/local/bin/kind
|
||||
kind version
|
||||
# https://packages.ubuntu.com/xenial/libnss3-tools
|
||||
- run: sudo apt install -y libnss3-tools
|
||||
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
|
||||
- run: make -C acceptance_test -j3 setup
|
||||
- run: make -C acceptance_test test
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,9 +1,10 @@
|
||||
/.idea
|
||||
|
||||
/.kubeconfig*
|
||||
/acceptance_test/output/
|
||||
|
||||
/dist
|
||||
/dist/output
|
||||
/coverage.out
|
||||
/gotest.log
|
||||
|
||||
/kubelogin
|
||||
/kubectl-oidc_login
|
||||
|
||||
51
DESIGN.md
51
DESIGN.md
@@ -1,51 +0,0 @@
|
||||
# Design of kubelogin
|
||||
|
||||
This explains design of kubelogin.
|
||||
|
||||
## Use cases
|
||||
|
||||
Kubelogin is a command line tool and designed to run as both a standalone command and a kubectl plugin.
|
||||
|
||||
It respects the following flags, commonly used in kubectl:
|
||||
|
||||
```
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
-v, --v int If set to 1 or greater, it shows debug log
|
||||
```
|
||||
|
||||
As well as it respects the environment variable `KUBECONFIG`.
|
||||
|
||||
|
||||
### Login by the command
|
||||
|
||||
TODO
|
||||
|
||||
### Wrap kubectl and login transparently
|
||||
|
||||
TODO
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
Kubelogin consists of the following layers:
|
||||
|
||||
- `usecases`: This provides the use-cases.
|
||||
- `adaptor`: This provides external access and converts objects between the use-cases and external system.
|
||||
|
||||
|
||||
### Use-cases
|
||||
|
||||
This provides the use-cases mentioned in the previous section.
|
||||
|
||||
This layer should not contain external access such as HTTP requests and system calls.
|
||||
|
||||
|
||||
### Adaptor
|
||||
|
||||
This provides external access such as command line interface and HTTP requests.
|
||||
|
||||
|
||||
65
Makefile
65
Makefile
@@ -1,40 +1,49 @@
|
||||
TARGET := kubelogin
|
||||
TARGET_PLUGIN := kubectl-oidc_login
|
||||
CIRCLE_TAG ?= HEAD
|
||||
LDFLAGS := -X main.version=$(CIRCLE_TAG)
|
||||
# CircleCI specific variables
|
||||
CIRCLE_TAG ?= latest
|
||||
GITHUB_USERNAME := $(CIRCLE_PROJECT_USERNAME)
|
||||
GITHUB_REPONAME := $(CIRCLE_PROJECT_REPONAME)
|
||||
|
||||
.PHONY: check run diagram release clean
|
||||
TARGET := kubelogin
|
||||
TARGET_OSARCH := linux_amd64 darwin_amd64 windows_amd64 linux_arm linux_arm64
|
||||
VERSION ?= $(CIRCLE_TAG)
|
||||
LDFLAGS := -X main.version=$(VERSION)
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
check:
|
||||
golangci-lint run
|
||||
$(MAKE) -C e2e_test/keys/testdata
|
||||
go test -v -race -cover -coverprofile=coverage.out ./...
|
||||
|
||||
$(TARGET): $(wildcard *.go)
|
||||
$(TARGET): $(wildcard **/*.go)
|
||||
go build -o $@ -ldflags "$(LDFLAGS)"
|
||||
|
||||
$(TARGET_PLUGIN): $(TARGET)
|
||||
ln -sf $(TARGET) $@
|
||||
.PHONY: check
|
||||
check:
|
||||
golangci-lint run
|
||||
go test -v -race -cover -coverprofile=coverage.out ./... > gotest.log
|
||||
|
||||
run: $(TARGET_PLUGIN)
|
||||
-PATH=.:$(PATH) kubectl oidc-login --help
|
||||
|
||||
diagram: docs/authn.png
|
||||
|
||||
%.png: %.seqdiag
|
||||
seqdiag -a -f /Library/Fonts/Verdana.ttf $<
|
||||
|
||||
dist:
|
||||
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
|
||||
mv dist/gh/kubelogin.rb dist/
|
||||
.PHONY: dist
|
||||
dist: dist/output
|
||||
dist/output:
|
||||
# make the zip files for GitHub Releases
|
||||
VERSION=$(VERSION) CGO_ENABLED=0 goxzst -d dist/output -i "LICENSE" -o "$(TARGET)" -osarch "$(TARGET_OSARCH)" -t "dist/kubelogin.rb dist/oidc-login.yaml dist/Dockerfile" -- -ldflags "$(LDFLAGS)"
|
||||
# test the zip file
|
||||
zipinfo dist/output/kubelogin_linux_amd64.zip
|
||||
# make the krew yaml structure
|
||||
mkdir -p dist/output/plugins
|
||||
mv dist/output/oidc-login.yaml dist/output/plugins/oidc-login.yaml
|
||||
|
||||
.PHONY: release
|
||||
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
|
||||
# publish the binaries
|
||||
ghcp release -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)" -t "$(VERSION)" dist/output/
|
||||
# publish the Homebrew formula
|
||||
ghcp commit -u "$(GITHUB_USERNAME)" -r "homebrew-$(GITHUB_REPONAME)" -b "bump-$(VERSION)" -m "Bump the version to $(VERSION)" -C dist/output/ kubelogin.rb
|
||||
ghcp pull-request -u "$(GITHUB_USERNAME)" -r "homebrew-$(GITHUB_REPONAME)" -b "bump-$(VERSION)" --title "Bump the version to $(VERSION)"
|
||||
# publish the Dockerfile
|
||||
ghcp commit -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)-docker" -b "bump-$(VERSION)" -m "Bump the version to $(VERSION)" -C dist/output/ Dockerfile
|
||||
ghcp pull-request -u "$(GITHUB_USERNAME)" -r "$(GITHUB_REPONAME)-docker" -b "bump-$(VERSION)" --title "Bump the version to $(VERSION)"
|
||||
# publish the Krew manifest
|
||||
ghcp fork-commit -u kubernetes-sigs -r krew-index -b "oidc-login-$(VERSION)" -m "Bump oidc-login to $(VERSION)" -C dist/output/ plugins/oidc-login.yaml
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm $(TARGET)
|
||||
-rm $(TARGET_PLUGIN)
|
||||
-rm -r dist/
|
||||
-rm -r dist/output/
|
||||
-rm coverage.out gotest.log
|
||||
|
||||
379
README.md
379
README.md
@@ -1,120 +1,110 @@
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin) [](https://goreportcard.com/report/github.com/int128/kubelogin)
|
||||
# kubelogin [](https://circleci.com/gh/int128/kubelogin)  [](https://goreportcard.com/report/github.com/int128/kubelogin)
|
||||
|
||||
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
|
||||
|
||||
In Kubernetes OIDC authentication, kubectl does not provide actual authentication and we need to manually set an ID token and refresh token to the kubeconfig.
|
||||
Kubelogin provides browser based authentication and writes an ID token and refresh token to the kubeconfig.
|
||||
Here is an example of Kubernetes authentication with the Google Identity Platform:
|
||||
|
||||
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/70971501-7bcebc80-20e4-11ea-8afc-539dcaea0aa8.gif" width="652" height="455">
|
||||
|
||||
Kubelogin is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
|
||||
When you run kubectl, kubelogin opens the browser and you can log in to the provider.
|
||||
Then kubelogin gets a token from the provider and kubectl access Kubernetes APIs with the token.
|
||||
Take a look at the diagram:
|
||||
|
||||

|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
You can install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
|
||||
### Setup
|
||||
|
||||
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
|
||||
|
||||
```sh
|
||||
# Homebrew
|
||||
brew tap int128/kubelogin
|
||||
brew install kubelogin
|
||||
brew install int128/kubelogin/kubelogin
|
||||
|
||||
# Krew
|
||||
kubectl krew install oidc-login
|
||||
|
||||
# GitHub Releases
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.12.0/kubelogin_linux_amd64.zip
|
||||
curl -LO https://github.com/int128/kubelogin/releases/download/v1.19.0/kubelogin_linux_amd64.zip
|
||||
unzip kubelogin_linux_amd64.zip
|
||||
ln -s kubelogin kubectl-oidc_login
|
||||
```
|
||||
|
||||
You need to configure the OIDC provider, Kubernetes API server, kubectl authentication and role binding.
|
||||
See the following documents for more:
|
||||
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
|
||||
The kubeconfig looks like:
|
||||
|
||||
- [Getting Started with Keycloak](docs/keycloak.md)
|
||||
- [Getting Started with Google Identity Platform](docs/google.md)
|
||||
- [Team Operation](docs/team_ops.md)
|
||||
|
||||
|
||||
### Login by the command
|
||||
|
||||
Just run the command:
|
||||
|
||||
```sh
|
||||
kubelogin
|
||||
|
||||
# or run as a kubectl plugin
|
||||
kubectl oidc-login
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubectl
|
||||
args:
|
||||
- oidc-login
|
||||
- get-token
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
It automatically opens the browser and you can log in to the provider.
|
||||
See [the setup guide](docs/setup.md) for more.
|
||||
|
||||
|
||||
### Run
|
||||
|
||||
Run kubectl.
|
||||
|
||||
```sh
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
Kubectl executes kubelogin before calling the Kubernetes APIs.
|
||||
Kubelogin automatically opens the browser and you can log in to the provider.
|
||||
|
||||
<img src="docs/keycloak-login.png" alt="keycloak-login" width="455" height="329">
|
||||
|
||||
After authentication, it writes an ID token and refresh token to the kubeconfig.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now you can access to the cluster.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
If the ID token is valid, kubelogin does nothing.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
```
|
||||
|
||||
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
|
||||
### Wrap kubectl and login transparently
|
||||
|
||||
You can wrap kubectl to transparently login to the provider.
|
||||
|
||||
```sh
|
||||
alias kubectl='kubelogin exec -- kubectl'
|
||||
|
||||
# or run as a kubectl plugin
|
||||
alias kubectl='kubectl oidc-login exec -- kubectl'
|
||||
```
|
||||
|
||||
If the token expired, it updates the kubeconfig and executes kubectl.
|
||||
After authentication, kubelogin returns the credentials to kubectl and finally kubectl calls the Kubernetes APIs with the credential.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-06-05 19:05:34 +0900 JST
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
If the ID token is valid, it just executes kubectl.
|
||||
Kubelogin writes the ID token and refresh token to the token cache file.
|
||||
|
||||
If the cached ID token is valid, kubelogin just returns it.
|
||||
If the cached ID token has expired, kubelogin will refresh the token using the refresh token.
|
||||
If the refresh token has expired, kubelogin will perform reauthentication.
|
||||
|
||||
|
||||
### Troubleshoot
|
||||
|
||||
You can log out by removing the token cache directory (default `~/.kube/cache/oidc-login`).
|
||||
Kubelogin will perform authentication if the token cache file does not exist.
|
||||
|
||||
You can dump the claims of token by passing `-v1` option.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
I0221 21:54:08.151850 28231 get_token.go:104] you got a token: {
|
||||
"sub": "********",
|
||||
"iss": "https://accounts.google.com",
|
||||
"aud": "********",
|
||||
"iat": 1582289639,
|
||||
"exp": 1582293239,
|
||||
"jti": "********",
|
||||
"nonce": "********",
|
||||
"at_hash": "********"
|
||||
}
|
||||
```
|
||||
|
||||
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
Kubelogin respects kubectl options passed to the extra arguments.
|
||||
For example, if you run `kubectl --kubeconfig .kubeconfig`,
|
||||
it will update `.kubeconfig` and execute kubectl.
|
||||
|
||||
If the current auth provider is not `oidc`, it just executes kubectl.
|
||||
|
||||
|
||||
## Configuration
|
||||
## Usage
|
||||
|
||||
This document is for the development version.
|
||||
If you are looking for a specific version, see [the release tags](https://github.com/int128/kubelogin/tags).
|
||||
@@ -123,68 +113,66 @@ Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
Usage:
|
||||
kubelogin [flags]
|
||||
kubelogin [command]
|
||||
|
||||
Examples:
|
||||
# Login to the provider using authorization code grant.
|
||||
kubelogin
|
||||
|
||||
# Login to the provider using resource owner password credentials grant.
|
||||
kubelogin --username USERNAME --password PASSWORD
|
||||
|
||||
# Wrap kubectl and login transparently
|
||||
alias kubectl='kubelogin exec -- kubectl'
|
||||
|
||||
Available Commands:
|
||||
exec Login transparently and execute the kubectl command
|
||||
help Help about any command
|
||||
version Print the version information
|
||||
kubelogin get-token [flags]
|
||||
|
||||
Flags:
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
-v, --v int If set to 1 or greater, it shows debug log
|
||||
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
-h, --help help for kubelogin
|
||||
--oidc-issuer-url string Issuer URL of the provider (mandatory)
|
||||
--oidc-client-id string Client ID of the provider (mandatory)
|
||||
--oidc-client-secret string Client secret of the provider
|
||||
--oidc-extra-scope strings Scopes to request to the provider
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--certificate-authority-data string Base64 encoded data 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
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--listen-port ints (Deprecated: use --listen-address)
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--oidc-redirect-url-hostname string Hostname of the redirect URL (default "localhost")
|
||||
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
-h, --help help for get-token
|
||||
|
||||
Global Flags:
|
||||
--add_dir_header If true, adds the file directory to the header
|
||||
--alsologtostderr log to standard error as well as files
|
||||
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
|
||||
--log_dir string If non-empty, write log files in this directory
|
||||
--log_file string If non-empty, use this log file
|
||||
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
|
||||
--logtostderr log to standard error instead of files (default true)
|
||||
--skip_headers If true, avoid header prefixes in the log messages
|
||||
--skip_log_headers If true, avoid headers when opening log files
|
||||
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
|
||||
-v, --v Level number for the log level verbosity
|
||||
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
|
||||
```
|
||||
|
||||
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.
|
||||
See also the options of [standalone mode](docs/standalone-mode.md).
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
|
||||
`client-id` | Read (Mandatory) | Client ID of the provider.
|
||||
`client-secret` | Read (Mandatory) | Client Secret of the provider.
|
||||
`idp-certificate-authority` | Read | CA certificate path of the provider.
|
||||
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
|
||||
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
|
||||
`id-token` | Write | ID token got from the provider.
|
||||
`refresh-token` | Write | Refresh token got from the provider.
|
||||
### Extra scopes
|
||||
|
||||
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
|
||||
|
||||
### Kubeconfig
|
||||
|
||||
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
|
||||
It defaults to `~/.kube/config`.
|
||||
|
||||
```sh
|
||||
# by the option
|
||||
kubelogin --kubeconfig /path/to/kubeconfig
|
||||
|
||||
# by the environment variable
|
||||
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
|
||||
```yaml
|
||||
- --oidc-extra-scope=email
|
||||
- --oidc-extra-scope=profile
|
||||
```
|
||||
|
||||
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.
|
||||
### CA Certificate
|
||||
|
||||
You can use your self-signed certificate for the provider.
|
||||
|
||||
```yaml
|
||||
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
|
||||
- --certificate-authority-data=LS0t...
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
||||
### Authentication flows
|
||||
|
||||
@@ -198,63 +186,128 @@ You need to register the following redirect URIs to the provider:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if port 8000 is already in use)
|
||||
|
||||
You can change the ports by the option:
|
||||
You can change the listening address.
|
||||
|
||||
```sh
|
||||
kubelogin --listen-port 12345 --listen-port 23456
|
||||
```yaml
|
||||
- --listen-address=127.0.0.1:12345
|
||||
- --listen-address=127.0.0.1:23456
|
||||
```
|
||||
|
||||
You can change the hostname of redirect URI from the default value `localhost`.
|
||||
|
||||
```yaml
|
||||
- --oidc-redirect-url-hostname=127.0.0.1
|
||||
```
|
||||
|
||||
You can add extra parameters to the authentication request.
|
||||
|
||||
```yaml
|
||||
- --oidc-auth-request-extra-params=ttl=86400
|
||||
```
|
||||
|
||||
#### Authorization code flow with keyboard interactive
|
||||
|
||||
If you cannot access the browser, instead use the authorization code flow with keyboard interactive.
|
||||
|
||||
```yaml
|
||||
- --grant-type=authcode-keyboard
|
||||
```
|
||||
|
||||
Kubelogin will show the URL and prompt.
|
||||
Open the URL in the browser and then copy the code shown.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
Open https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&client_id=...
|
||||
Enter code: YOUR_CODE
|
||||
```
|
||||
|
||||
Note that this flow uses the redirect URI `urn:ietf:wg:oauth:2.0:oob` and
|
||||
some OIDC providers do not support it.
|
||||
|
||||
You can add extra parameters to the authentication request.
|
||||
|
||||
```yaml
|
||||
- --oidc-auth-request-extra-params=ttl=86400
|
||||
```
|
||||
|
||||
#### Resource owner password credentials grant flow
|
||||
|
||||
As well as you can use the resource owner password credentials grant flow.
|
||||
Kubelogin performs the resource owner password credentials grant flow
|
||||
when `--grant-type=password` or `--username` is set.
|
||||
|
||||
Note that most OIDC providers do not support this flow.
|
||||
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
|
||||
Most OIDC providers do not support this flow.
|
||||
|
||||
You can pass the username and password:
|
||||
You can set the username and password.
|
||||
|
||||
```
|
||||
% kubelogin --username USER --password PASS
|
||||
```yaml
|
||||
- --username=USERNAME
|
||||
- --password=PASSWORD
|
||||
```
|
||||
|
||||
or use the password prompt:
|
||||
If the password is not set, kubelogin will show the prompt for the password.
|
||||
|
||||
```yaml
|
||||
- --username=USERNAME
|
||||
```
|
||||
|
||||
```
|
||||
% kubelogin --username USER
|
||||
% kubectl get pods
|
||||
Password:
|
||||
```
|
||||
|
||||
If the username is not set, kubelogin will show the prompt for the username and password.
|
||||
|
||||
### Extra scopes
|
||||
|
||||
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
|
||||
```yaml
|
||||
- --grant-type=password
|
||||
```
|
||||
|
||||
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
|
||||
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
|
||||
```
|
||||
% kubectl get pods
|
||||
Username: foo
|
||||
Password:
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
### CA Certificates
|
||||
You can run [the Docker image](https://quay.io/repository/int128/kubelogin) instead of the binary.
|
||||
The kubeconfig looks like:
|
||||
|
||||
You can use your self-signed certificates for the provider.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak \
|
||||
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
|
||||
```yaml
|
||||
users:
|
||||
- name: oidc
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: docker
|
||||
args:
|
||||
- run
|
||||
- --rm
|
||||
- -v
|
||||
- /tmp/.token-cache:/.token-cache
|
||||
- -p
|
||||
- 8000:8000
|
||||
- quay.io/int128/kubelogin:v1.19.0
|
||||
- get-token
|
||||
- --token-cache-dir=/.token-cache
|
||||
- --listen-address=0.0.0.0:8000
|
||||
- --oidc-issuer-url=ISSUER_URL
|
||||
- --oidc-client-id=YOUR_CLIENT_ID
|
||||
- --oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Known limitations:
|
||||
|
||||
### HTTP Proxy
|
||||
- It cannot open the browser automatically.
|
||||
- The container port and listen port must be equal for consistency of the redirect URI.
|
||||
|
||||
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).
|
||||
|
||||
## Related works
|
||||
|
||||
### Kubernetes Dashboard
|
||||
|
||||
You can access the Kubernetes Dashboard using kubelogin and [kauthproxy](https://github.com/int128/kauthproxy).
|
||||
|
||||
|
||||
## Contributions
|
||||
@@ -264,7 +317,7 @@ Feel free to open issues and pull requests for improving code and documents.
|
||||
|
||||
### Development
|
||||
|
||||
Go 1.12 or later is required.
|
||||
Go 1.13 or later is required.
|
||||
|
||||
```sh
|
||||
# Run lint and tests
|
||||
@@ -274,3 +327,5 @@ make check
|
||||
make
|
||||
./kubelogin
|
||||
```
|
||||
|
||||
See also [the acceptance test](acceptance_test).
|
||||
|
||||
109
acceptance_test/Makefile
Normal file
109
acceptance_test/Makefile
Normal file
@@ -0,0 +1,109 @@
|
||||
CLUSTER_NAME := kubelogin-acceptance-test
|
||||
OUTPUT_DIR := $(CURDIR)/output
|
||||
|
||||
PATH := $(PATH):$(OUTPUT_DIR)/bin
|
||||
export PATH
|
||||
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
|
||||
export KUBECONFIG
|
||||
|
||||
# run the login script instead of opening chrome
|
||||
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
|
||||
export BROWSER
|
||||
|
||||
.PHONY: test
|
||||
test: build
|
||||
# see the setup instruction
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--oidc-extra-scope=email \
|
||||
--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# set up the kubeconfig
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=https://dex-server:10443/dex \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
|
||||
--exec-arg=--oidc-extra-scope=email \
|
||||
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
|
||||
# make sure we can access the cluster
|
||||
kubectl --user=oidc cluster-info
|
||||
# switch the current context
|
||||
kubectl config set-context --current --user=oidc
|
||||
# make sure we can access the cluster
|
||||
kubectl cluster-info
|
||||
|
||||
.PHONY: setup
|
||||
setup: build dex cluster setup-chrome
|
||||
|
||||
.PHONY: setup-chrome
|
||||
setup-chrome: $(OUTPUT_DIR)/ca.crt
|
||||
# add the dex server certificate to the trust store
|
||||
mkdir -p ~/.pki/nssdb
|
||||
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
|
||||
|
||||
# build binaries
|
||||
.PHONY: build
|
||||
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
|
||||
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
|
||||
go build -o $@ ..
|
||||
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
|
||||
go build -o $@ ./chromelogin
|
||||
|
||||
# create a Dex server
|
||||
.PHONY: dex
|
||||
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
|
||||
docker create --name dex-server -p 10443:10443 quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
|
||||
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
|
||||
docker cp $(OUTPUT_DIR)/server.key dex-server:/
|
||||
docker cp dex.yaml dex-server:/
|
||||
docker start dex-server
|
||||
docker logs dex-server
|
||||
|
||||
$(OUTPUT_DIR)/ca.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
|
||||
$(OUTPUT_DIR)/server.key:
|
||||
mkdir -p $(OUTPUT_DIR)
|
||||
openssl genrsa -out $@ 2048
|
||||
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
|
||||
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
|
||||
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
|
||||
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
|
||||
|
||||
# create a Kubernetes cluster
|
||||
.PHONY: cluster
|
||||
cluster: dex create-cluster
|
||||
# add the Dex container IP to /etc/hosts of kube-apiserver
|
||||
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
|
||||
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
|
||||
# wait for kube-apiserver oidc initialization
|
||||
# (oidc authenticator will retry oidc discovery every 10s)
|
||||
sleep 10
|
||||
|
||||
.PHONY: create-cluster
|
||||
create-cluster: $(OUTPUT_DIR)/ca.crt
|
||||
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-acceptance-test-dex-ca.crt
|
||||
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
|
||||
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
|
||||
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=admin@example.com
|
||||
|
||||
# clean up the resources
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -r $(OUTPUT_DIR)
|
||||
.PHONY: delete-cluster
|
||||
delete-cluster:
|
||||
kind delete cluster --name $(CLUSTER_NAME)
|
||||
.PHONY: delete-dex
|
||||
delete-dex:
|
||||
docker stop dex-server
|
||||
docker rm dex-server
|
||||
109
acceptance_test/README.md
Normal file
109
acceptance_test/README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# kubelogin/acceptance_test
|
||||
|
||||
This is an acceptance test for walkthrough of the OIDC initial setup and plugin behavior using a real Kubernetes cluster and OpenID Connect provider, running on [GitHub Actions](https://github.com/int128/kubelogin/actions?query=workflow%3Aacceptance-test).
|
||||
|
||||
It is intended to verify the following points:
|
||||
|
||||
- User can set up Kubernetes OIDC authentication and this plugin.
|
||||
- User can access a cluster after login.
|
||||
|
||||
It performs the test using the following components:
|
||||
|
||||
- Kubernetes cluster (Kind)
|
||||
- OIDC provider (Dex)
|
||||
- Browser (Chrome)
|
||||
- kubectl command
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
Let's take a look at the diagram.
|
||||
|
||||

|
||||
|
||||
It prepares the following resources:
|
||||
|
||||
1. Generate a pair of CA certificate and TLS server certificate for Dex.
|
||||
1. Run Dex on a container.
|
||||
1. Create a Kubernetes cluster using Kind.
|
||||
1. Mutate `/etc/hosts` of the CI machine to access Dex.
|
||||
1. Mutate `/etc/hosts` of the kube-apiserver pod to access Dex.
|
||||
|
||||
It performs the test by the following steps:
|
||||
|
||||
1. Run kubectl.
|
||||
1. kubectl automatically runs kubelogin.
|
||||
1. kubelogin automatically runs [chromelogin](chromelogin).
|
||||
1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
|
||||
1. kubelogin gets an authorization code from the browser.
|
||||
1. kubelogin gets a token.
|
||||
1. kubectl accesses an API with the token.
|
||||
1. kube-apiserver verifies the token by Dex.
|
||||
1. Check if kubectl exited with code 0.
|
||||
|
||||
|
||||
## Run locally
|
||||
|
||||
You need to set up the following components:
|
||||
|
||||
- Docker
|
||||
- Kind
|
||||
- Chrome or Chromium
|
||||
|
||||
You need to add the following line to `/etc/hosts` so that the browser can access the Dex.
|
||||
|
||||
```
|
||||
127.0.0.1 dex-server
|
||||
```
|
||||
|
||||
Run the test.
|
||||
|
||||
```shell script
|
||||
# run the test
|
||||
make
|
||||
|
||||
# clean up
|
||||
make delete-cluster
|
||||
make delete-dex
|
||||
```
|
||||
|
||||
|
||||
## Technical consideration
|
||||
|
||||
### Network and DNS
|
||||
|
||||
Consider the following issues:
|
||||
|
||||
- kube-apiserver runs on the host network of the kind container.
|
||||
- kube-apiserver cannot resolve a service name by kube-dns.
|
||||
- kube-apiserver cannot access a cluster IP.
|
||||
- kube-apiserver can access another container via the Docker network.
|
||||
- Chrome requires exactly match of domain name between Dex URL and a server certificate.
|
||||
|
||||
Consequently,
|
||||
|
||||
- kube-apiserver accesses Dex by resolving `/etc/hosts` and via the Docker network.
|
||||
- kubelogin and Chrome accesses Dex by resolving `/etc/hosts` and via the Docker network.
|
||||
|
||||
### TLS server certificate
|
||||
|
||||
Consider the following issues:
|
||||
|
||||
- kube-apiserver requires `--oidc-issuer` is HTTPS URL.
|
||||
- kube-apiserver requires a CA certificate at startup, if `--oidc-ca-file` is given.
|
||||
- kube-apiserver mounts `/usr/local/share/ca-certificates` from the kind container.
|
||||
- It is possible to mount a file from the CI machine.
|
||||
- It is not possible to issue a certificate using Let's Encrypt in runtime.
|
||||
- Chrome requires a valid certificate in `~/.pki/nssdb`.
|
||||
|
||||
As a result,
|
||||
|
||||
- kube-apiserver uses the CA certificate of `/usr/local/share/ca-certificates/dex-ca.crt`. See the `extraMounts` section of [`cluster.yaml`](cluster.yaml).
|
||||
- kubelogin uses the CA certificate in `output/ca.crt`.
|
||||
- Chrome uses the CA certificate in `~/.pki/nssdb`.
|
||||
|
||||
### Test environment
|
||||
|
||||
- Set the issuer URL to kubectl. See [`kubeconfig_oidc.yaml`](kubeconfig_oidc.yaml).
|
||||
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
|
||||
- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.
|
||||
93
acceptance_test/chromelogin/main.go
Normal file
93
acceptance_test/chromelogin/main.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatalf("usage: %s URL", os.Args[0])
|
||||
return
|
||||
}
|
||||
url := os.Args[1]
|
||||
if err := runBrowser(context.Background(), url); err != nil {
|
||||
log.Fatalf("error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runBrowser(ctx context.Context, url string) error {
|
||||
execOpts := chromedp.DefaultExecAllocatorOptions[:]
|
||||
execOpts = append(execOpts, chromedp.NoSandbox)
|
||||
ctx, cancel := chromedp.NewExecAllocator(ctx, execOpts...)
|
||||
defer cancel()
|
||||
ctx, cancel = chromedp.NewContext(ctx, chromedp.WithLogf(log.Printf))
|
||||
defer cancel()
|
||||
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if err := logInToDex(ctx, url); err != nil {
|
||||
return fmt.Errorf("could not run the browser: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logInToDex(ctx context.Context, url string) error {
|
||||
for {
|
||||
var location string
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(url),
|
||||
chromedp.Location(&location),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("location: %s", location)
|
||||
if strings.HasPrefix(location, `http://`) || strings.HasPrefix(location, `https://`) {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
err := chromedp.Run(ctx,
|
||||
// https://dex-server:10443/dex/auth/local
|
||||
chromedp.WaitVisible(`#login`),
|
||||
logPageMetadata(),
|
||||
chromedp.SendKeys(`#login`, `admin@example.com`),
|
||||
chromedp.SendKeys(`#password`, `password`),
|
||||
chromedp.Submit(`#submit-login`),
|
||||
// https://dex-server:10443/dex/approval
|
||||
chromedp.WaitVisible(`.dex-btn.theme-btn--success`),
|
||||
logPageMetadata(),
|
||||
chromedp.Submit(`.dex-btn.theme-btn--success`),
|
||||
// http://localhost:8000
|
||||
chromedp.WaitReady(`body`),
|
||||
logPageMetadata(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func logPageMetadata() chromedp.Action {
|
||||
var location string
|
||||
var title string
|
||||
return chromedp.Tasks{
|
||||
chromedp.Location(&location),
|
||||
chromedp.Title(&title),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
log.Printf("location: %s [%s]", location, title)
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
20
acceptance_test/cluster.yaml
Normal file
20
acceptance_test/cluster.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
# https://github.com/dexidp/dex/blob/master/Documentation/kubernetes.md
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
apiVersion: kubeadm.k8s.io/v1beta2
|
||||
kind: ClusterConfiguration
|
||||
metadata:
|
||||
name: config
|
||||
apiServer:
|
||||
extraArgs:
|
||||
oidc-issuer-url: https://dex-server:10443/dex
|
||||
oidc-client-id: YOUR_CLIENT_ID
|
||||
oidc-username-claim: email
|
||||
oidc-ca-file: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
nodes:
|
||||
- role: control-plane
|
||||
extraMounts:
|
||||
- hostPath: /tmp/kubelogin-acceptance-test-dex-ca.crt
|
||||
containerPath: /usr/local/share/ca-certificates/dex-ca.crt
|
||||
23
acceptance_test/dex.yaml
Normal file
23
acceptance_test/dex.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
issuer: https://dex-server:10443/dex
|
||||
web:
|
||||
https: 0.0.0.0:10443
|
||||
tlsCert: /server.crt
|
||||
tlsKey: /server.key
|
||||
storage:
|
||||
type: sqlite3
|
||||
config:
|
||||
file: /tmp/dex.db
|
||||
staticClients:
|
||||
- id: YOUR_CLIENT_ID
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
name: kubelogin
|
||||
secret: YOUR_CLIENT_SECRET
|
||||
staticPasswords:
|
||||
- email: "admin@example.com"
|
||||
# bcrypt hash of the string "password"
|
||||
hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
|
||||
username: "admin"
|
||||
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
|
||||
# required for staticPasswords
|
||||
enablePasswordDB: true
|
||||
15
acceptance_test/openssl.cnf
Normal file
15
acceptance_test/openssl.cnf
Normal file
@@ -0,0 +1,15 @@
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = @alt_names
|
||||
extendedKeyUsage = serverAuth
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = dex-server
|
||||
DNS.2 = dex-server:30443
|
||||
@@ -1,180 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Cmd.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Cmd), "*"),
|
||||
wire.Bind(new(adaptors.Cmd), new(*Cmd)),
|
||||
)
|
||||
|
||||
const examples = ` # Login to the provider using authorization code grant.
|
||||
%[1]s
|
||||
|
||||
# Login to the provider using resource owner password credentials grant.
|
||||
%[1]s --username USERNAME --password PASSWORD
|
||||
|
||||
# Wrap kubectl and login transparently
|
||||
alias kubectl='%[1]s exec -- kubectl'`
|
||||
|
||||
var defaultListenPort = []int{8000, 18000}
|
||||
|
||||
// Cmd provides interaction with command line interface (CLI).
|
||||
type Cmd struct {
|
||||
Login usecases.Login
|
||||
LoginAndExec usecases.LoginAndExec
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
// Run parses the command line arguments and executes the specified use-case.
|
||||
// It returns an exit code, that is 0 on success or 1 on error.
|
||||
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
var exitCode int
|
||||
executable := filepath.Base(args[0])
|
||||
var o struct {
|
||||
kubectlOptions
|
||||
kubeloginOptions
|
||||
}
|
||||
rootCmd := cobra.Command{
|
||||
Use: executable,
|
||||
Short: "Login to the OpenID Connect provider and update the kubeconfig",
|
||||
Example: fmt.Sprintf(examples, executable),
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
in := usecases.LoginIn{
|
||||
KubeconfigFilename: o.Kubeconfig,
|
||||
KubeconfigContext: kubeconfig.ContextName(o.Context),
|
||||
KubeconfigUser: kubeconfig.UserName(o.User),
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
if err := cmd.Login.Do(ctx, in); err != nil {
|
||||
cmd.Logger.Printf("error: %s", err)
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
o.kubectlOptions.register(rootCmd.Flags())
|
||||
o.kubeloginOptions.register(rootCmd.Flags())
|
||||
|
||||
execCmd := cobra.Command{
|
||||
Use: "exec [flags] -- kubectl [args]",
|
||||
Short: "Login transparently and execute the kubectl command",
|
||||
Args: func(execCmd *cobra.Command, args []string) error {
|
||||
if execCmd.ArgsLenAtDash() == -1 {
|
||||
return xerrors.Errorf("double dash is missing, please run as %s exec -- kubectl", executable)
|
||||
}
|
||||
if len(args) < 1 {
|
||||
return xerrors.New("too few arguments")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(execCmd *cobra.Command, args []string) {
|
||||
// parse the extra args and override the kubectl options
|
||||
f := pflag.NewFlagSet(execCmd.Name(), pflag.ContinueOnError)
|
||||
o.kubectlOptions.register(f)
|
||||
// ignore unknown flags and help flags (-h/--help)
|
||||
f.ParseErrorsWhitelist.UnknownFlags = true
|
||||
f.BoolP("help", "h", false, "ignore help flags")
|
||||
if err := f.Parse(args); err != nil {
|
||||
cmd.Logger.Debugf(1, "error while parsing the extra arguments: %s", err)
|
||||
}
|
||||
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
|
||||
in := usecases.LoginAndExecIn{
|
||||
LoginIn: usecases.LoginIn{
|
||||
KubeconfigFilename: o.Kubeconfig,
|
||||
KubeconfigContext: kubeconfig.ContextName(o.Context),
|
||||
KubeconfigUser: kubeconfig.UserName(o.User),
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
ListenPort: o.ListenPort,
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
},
|
||||
Executable: args[0],
|
||||
Args: args[1:],
|
||||
}
|
||||
out, err := cmd.LoginAndExec.Do(ctx, in)
|
||||
if err != nil {
|
||||
cmd.Logger.Printf("error: %s", err)
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
exitCode = out.ExitCode
|
||||
},
|
||||
}
|
||||
o.kubeloginOptions.register(execCmd.Flags())
|
||||
rootCmd.AddCommand(&execCmd)
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
cmd.Logger.Printf("%s version %s", executable, version)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(&versionCmd)
|
||||
|
||||
rootCmd.SetArgs(args[1:])
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
cmd.Logger.Debugf(1, "error while parsing the arguments: %s", err)
|
||||
return 1
|
||||
}
|
||||
return exitCode
|
||||
}
|
||||
|
||||
// kubectlOptions represents kubectl specific options.
|
||||
type kubectlOptions struct {
|
||||
Kubeconfig string
|
||||
Context string
|
||||
User string
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
Verbose int
|
||||
}
|
||||
|
||||
func (o *kubectlOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
|
||||
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
|
||||
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
f.IntVarP(&o.Verbose, "v", "v", 0, "If set to 1 or greater, it shows debug log")
|
||||
}
|
||||
|
||||
// kubeloginOptions represents application specific options.
|
||||
type kubeloginOptions struct {
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (o *kubeloginOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
|
||||
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
|
||||
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
|
||||
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/mock_usecases"
|
||||
)
|
||||
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
const executable = "kubelogin"
|
||||
const version = "HEAD"
|
||||
|
||||
t.Run("login/Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
login := mock_usecases.NewMockLogin(ctrl)
|
||||
login.EXPECT().
|
||||
Do(ctx, usecases.LoginIn{
|
||||
ListenPort: defaultListenPort,
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
Login: login,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("login/FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
login := mock_usecases.NewMockLogin(ctrl)
|
||||
login.EXPECT().
|
||||
Do(ctx, usecases.LoginIn{
|
||||
KubeconfigFilename: "/path/to/kubeconfig",
|
||||
KubeconfigContext: "hello.k8s.local",
|
||||
KubeconfigUser: "google",
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: []int{10080, 20080},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
})
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(1))
|
||||
|
||||
cmd := Cmd{
|
||||
Login: login,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"--kubeconfig", "/path/to/kubeconfig",
|
||||
"--context", "hello.k8s.local",
|
||||
"--user", "google",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("login/TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
cmd := Cmd{
|
||||
Login: mock_usecases.NewMockLogin(ctrl),
|
||||
Logger: mock_adaptors.NewLogger(t, ctrl),
|
||||
}
|
||||
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loginAndExec/Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
|
||||
loginAndExec.EXPECT().
|
||||
Do(ctx, usecases.LoginAndExecIn{
|
||||
LoginIn: usecases.LoginIn{
|
||||
ListenPort: defaultListenPort,
|
||||
},
|
||||
Executable: "kubectl",
|
||||
Args: []string{"dummy"},
|
||||
}).
|
||||
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(0))
|
||||
|
||||
cmd := Cmd{
|
||||
LoginAndExec: loginAndExec,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "exec", "--", "kubectl", "dummy"}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loginAndExec/OptionsInExtraArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
|
||||
loginAndExec.EXPECT().
|
||||
Do(ctx, usecases.LoginAndExecIn{
|
||||
LoginIn: usecases.LoginIn{
|
||||
KubeconfigFilename: "/path/to/kubeconfig2",
|
||||
KubeconfigContext: "hello2.k8s.local",
|
||||
KubeconfigUser: "google2",
|
||||
CACertFilename: "/path/to/cacert2",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: defaultListenPort,
|
||||
},
|
||||
Executable: "kubectl",
|
||||
Args: []string{
|
||||
"--kubeconfig", "/path/to/kubeconfig2",
|
||||
"--context", "hello2.k8s.local",
|
||||
"--user", "google2",
|
||||
"--certificate-authority", "/path/to/cacert2",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v2",
|
||||
"--listen-port", "30080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER2",
|
||||
"--password", "PASS2",
|
||||
"dummy",
|
||||
"--dummy",
|
||||
"--help",
|
||||
},
|
||||
}).
|
||||
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
|
||||
|
||||
cmd := Cmd{
|
||||
LoginAndExec: loginAndExec,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"exec",
|
||||
"--",
|
||||
"kubectl",
|
||||
// kubectl options in the extra args should be mapped to the options
|
||||
"--kubeconfig", "/path/to/kubeconfig2",
|
||||
"--context", "hello2.k8s.local",
|
||||
"--user", "google2",
|
||||
"--certificate-authority", "/path/to/cacert2",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v2",
|
||||
// kubelogin options in the extra args should not affect
|
||||
"--listen-port", "30080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER2",
|
||||
"--password", "PASS2",
|
||||
"dummy",
|
||||
"--dummy",
|
||||
"--help",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loginAndExec/OverrideOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
|
||||
loginAndExec := mock_usecases.NewMockLoginAndExec(ctrl)
|
||||
loginAndExec.EXPECT().
|
||||
Do(ctx, usecases.LoginAndExecIn{
|
||||
LoginIn: usecases.LoginIn{
|
||||
KubeconfigFilename: "/path/to/kubeconfig2",
|
||||
KubeconfigContext: "hello2.k8s.local",
|
||||
KubeconfigUser: "google2",
|
||||
CACertFilename: "/path/to/cacert2",
|
||||
SkipTLSVerify: true,
|
||||
ListenPort: []int{10080, 20080},
|
||||
SkipOpenBrowser: true,
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
Executable: "kubectl",
|
||||
Args: []string{
|
||||
"--kubeconfig", "/path/to/kubeconfig2",
|
||||
"--context", "hello2.k8s.local",
|
||||
"--user", "google2",
|
||||
"--certificate-authority", "/path/to/cacert2",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v2",
|
||||
"--listen-port", "30080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER2",
|
||||
"--password", "PASS2",
|
||||
"dummy",
|
||||
"--dummy",
|
||||
},
|
||||
}).
|
||||
Return(&usecases.LoginAndExecOut{ExitCode: 0}, nil)
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().SetLevel(adaptors.LogLevel(2))
|
||||
|
||||
cmd := Cmd{
|
||||
LoginAndExec: loginAndExec,
|
||||
Logger: logger,
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
// kubelogin options in the first args should be mapped to the options
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
"exec",
|
||||
"--",
|
||||
"kubectl",
|
||||
// kubectl options in the extra args should be mapped to the options
|
||||
"--kubeconfig", "/path/to/kubeconfig2",
|
||||
"--context", "hello2.k8s.local",
|
||||
"--user", "google2",
|
||||
"--certificate-authority", "/path/to/cacert2",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v2",
|
||||
// kubelogin options in the extra args should not affect
|
||||
"--listen-port", "30080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER2",
|
||||
"--password", "PASS2",
|
||||
"dummy",
|
||||
"--dummy",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
54
adaptors/env/env.go
vendored
54
adaptors/env/env.go
vendored
@@ -1,54 +0,0 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Env.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Env), "*"),
|
||||
wire.Bind(new(adaptors.Env), new(*Env)),
|
||||
)
|
||||
|
||||
// Env provides environment specific facilities.
|
||||
type Env struct{}
|
||||
|
||||
// ReadPassword reads a password from the stdin without echo back.
|
||||
func (*Env) ReadPassword(prompt string) (string, error) {
|
||||
if _, err := fmt.Fprint(os.Stderr, "Password: "); err != nil {
|
||||
return "", xerrors.Errorf("could not write the prompt: %w", err)
|
||||
}
|
||||
b, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("could not read: %w", err)
|
||||
}
|
||||
if _, err := fmt.Fprintln(os.Stderr); err != nil {
|
||||
return "", xerrors.Errorf("could not write a new line: %w", err)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Exec executes the command and returns the exit code.
|
||||
// Unlike the exec package, this does not return an error even if the command exited with non-zero code.
|
||||
func (*Env) Exec(ctx context.Context, executable string, args []string) (int, error) {
|
||||
c := exec.CommandContext(ctx, executable, args...)
|
||||
c.Stdin = os.Stdin
|
||||
c.Stdout = os.Stdout
|
||||
c.Stderr = os.Stderr
|
||||
if err := c.Run(); err != nil {
|
||||
if err, ok := err.(*exec.ExitError); ok {
|
||||
return err.ExitCode(), nil
|
||||
}
|
||||
return 0, xerrors.Errorf("could not execute the command: %w", err)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package adaptors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors Kubeconfig,OIDC,OIDCClient,Env,Logger
|
||||
|
||||
type Cmd interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
}
|
||||
|
||||
type Kubeconfig interface {
|
||||
GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error)
|
||||
UpdateAuthProvider(auth *kubeconfig.AuthProvider) error
|
||||
}
|
||||
|
||||
type OIDC interface {
|
||||
New(ctx context.Context, config OIDCClientConfig) (OIDCClient, error)
|
||||
}
|
||||
|
||||
// OIDCClientConfig represents a configuration of an OIDCClient to create.
|
||||
type OIDCClientConfig struct {
|
||||
Config kubeconfig.OIDCConfig
|
||||
CACertFilename string
|
||||
SkipTLSVerify bool
|
||||
}
|
||||
|
||||
type OIDCClient interface {
|
||||
AuthenticateByCode(ctx context.Context, in OIDCAuthenticateByCodeIn) (*OIDCAuthenticateOut, error)
|
||||
AuthenticateByPassword(ctx context.Context, in OIDCAuthenticateByPasswordIn) (*OIDCAuthenticateOut, error)
|
||||
Verify(ctx context.Context, in OIDCVerifyIn) (*OIDCVerifyOut, error)
|
||||
Refresh(ctx context.Context, in OIDCRefreshIn) (*OIDCAuthenticateOut, error)
|
||||
}
|
||||
|
||||
// OIDCAuthenticateByCodeIn represents an input DTO of OIDCClient.AuthenticateByCode.
|
||||
type OIDCAuthenticateByCodeIn struct {
|
||||
LocalServerPort []int // HTTP server port candidates
|
||||
SkipOpenBrowser bool // skip opening browser if true
|
||||
ShowLocalServerURL interface{ ShowLocalServerURL(url string) }
|
||||
}
|
||||
|
||||
// OIDCAuthenticateByPasswordIn represents an input DTO of OIDCClient.AuthenticateByPassword.
|
||||
type OIDCAuthenticateByPasswordIn struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// OIDCAuthenticateOut represents an output DTO of
|
||||
// OIDCClient.AuthenticateByCode, OIDCClient.AuthenticateByPassword and OIDCClient.Refresh.
|
||||
type OIDCAuthenticateOut struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
// OIDCVerifyIn represents an input DTO of OIDCClient.Verify.
|
||||
type OIDCVerifyIn struct {
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
// OIDCVerifyIn represents an output DTO of OIDCClient.Verify.
|
||||
type OIDCVerifyOut struct {
|
||||
IDTokenExpiry time.Time
|
||||
IDTokenClaims map[string]string // string representation of claims for logging
|
||||
}
|
||||
|
||||
// OIDCRefreshIn represents an input DTO of OIDCClient.Refresh.
|
||||
type OIDCRefreshIn struct {
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
type Env interface {
|
||||
ReadPassword(prompt string) (string, error)
|
||||
Exec(ctx context.Context, executable string, args []string) (int, error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
Debugf(level LogLevel, format string, v ...interface{})
|
||||
SetLevel(level LogLevel)
|
||||
IsEnabled(level LogLevel) bool
|
||||
}
|
||||
|
||||
// LogLevel represents a log level for debug.
|
||||
//
|
||||
// 0 = None
|
||||
// 1 = Including in/out
|
||||
// 2 = Including transport headers
|
||||
// 3 = Including transport body
|
||||
//
|
||||
type LogLevel int
|
||||
@@ -1,14 +0,0 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Kubeconfig), "*"),
|
||||
wire.Bind(new(adaptors.Kubeconfig), new(*Kubeconfig)),
|
||||
)
|
||||
|
||||
type Kubeconfig struct{}
|
||||
@@ -1,51 +0,0 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func (*Kubeconfig) UpdateAuthProvider(auth *kubeconfig.AuthProvider) error {
|
||||
config, err := clientcmd.LoadFromFile(auth.LocationOfOrigin)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not load %s: %w", auth.LocationOfOrigin, err)
|
||||
}
|
||||
userNode, ok := config.AuthInfos[string(auth.UserName)]
|
||||
if !ok {
|
||||
return xerrors.Errorf("user %s does not exist", auth.UserName)
|
||||
}
|
||||
if userNode.AuthProvider == nil {
|
||||
return xerrors.Errorf("auth-provider is missing")
|
||||
}
|
||||
if userNode.AuthProvider.Name != "oidc" {
|
||||
return xerrors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
|
||||
}
|
||||
copyOIDCConfig(auth.OIDCConfig, userNode.AuthProvider.Config)
|
||||
if err := clientcmd.WriteToFile(*config, auth.LocationOfOrigin); err != nil {
|
||||
return xerrors.Errorf("could not update %s: %w", auth.LocationOfOrigin, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyOIDCConfig(config kubeconfig.OIDCConfig, m map[string]string) {
|
||||
setOrDeleteKey(m, "idp-issuer-url", config.IDPIssuerURL)
|
||||
setOrDeleteKey(m, "client-id", config.ClientID)
|
||||
setOrDeleteKey(m, "client-secret", config.ClientSecret)
|
||||
setOrDeleteKey(m, "idp-certificate-authority", config.IDPCertificateAuthority)
|
||||
setOrDeleteKey(m, "idp-certificate-authority-data", config.IDPCertificateAuthorityData)
|
||||
extraScopes := strings.Join(config.ExtraScopes, ",")
|
||||
setOrDeleteKey(m, "extra-scopes", extraScopes)
|
||||
setOrDeleteKey(m, "id-token", config.IDToken)
|
||||
setOrDeleteKey(m, "refresh-token", config.RefreshToken)
|
||||
}
|
||||
|
||||
func setOrDeleteKey(m map[string]string, key, value string) {
|
||||
if value == "" {
|
||||
delete(m, key)
|
||||
return
|
||||
}
|
||||
m[key] = value
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Logger.
|
||||
var Set = wire.NewSet(
|
||||
New,
|
||||
)
|
||||
|
||||
// New returns a Logger with the standard log.Logger for messages and debug.
|
||||
func New() adaptors.Logger {
|
||||
return &Logger{
|
||||
stdLogger: log.New(os.Stderr, "", 0),
|
||||
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds|log.Lshortfile),
|
||||
}
|
||||
}
|
||||
|
||||
func NewWith(s stdLogger, d debugLogger) *Logger {
|
||||
return &Logger{s, d, 0}
|
||||
}
|
||||
|
||||
type stdLogger interface {
|
||||
Printf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type debugLogger interface {
|
||||
Output(calldepth int, s string) error
|
||||
}
|
||||
|
||||
// Logger wraps the standard log.Logger and just provides debug level.
|
||||
type Logger struct {
|
||||
stdLogger
|
||||
debugLogger
|
||||
level adaptors.LogLevel
|
||||
}
|
||||
|
||||
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
|
||||
if l.IsEnabled(level) {
|
||||
_ = l.debugLogger.Output(2, fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) SetLevel(level adaptors.LogLevel) {
|
||||
l.level = level
|
||||
}
|
||||
|
||||
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
|
||||
return level <= l.level
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
type mockDebugLogger struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (l *mockDebugLogger) Output(int, string) error {
|
||||
l.count++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestLogger_Debugf(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
loggerLevel adaptors.LogLevel
|
||||
debugfLevel adaptors.LogLevel
|
||||
count int
|
||||
}{
|
||||
{0, 0, 1},
|
||||
{0, 1, 0},
|
||||
|
||||
{1, 0, 1},
|
||||
{1, 1, 1},
|
||||
{1, 2, 0},
|
||||
|
||||
{2, 1, 1},
|
||||
{2, 2, 1},
|
||||
{2, 3, 0},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%+v", c), func(t *testing.T) {
|
||||
m := &mockDebugLogger{}
|
||||
l := &Logger{debugLogger: m, level: c.loggerLevel}
|
||||
l.Debugf(c.debugfLevel, "hello")
|
||||
if m.count != c.count {
|
||||
t.Errorf("count wants %d but %d", c.count, m.count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockStdLogger struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (l *mockStdLogger) Printf(format string, v ...interface{}) {
|
||||
l.count++
|
||||
}
|
||||
|
||||
func TestLogger_Printf(t *testing.T) {
|
||||
m := &mockStdLogger{}
|
||||
l := &Logger{stdLogger: m}
|
||||
l.Printf("hello")
|
||||
if m.count != 1 {
|
||||
t.Errorf("count wants %d but %d", 1, m.count)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package mock_adaptors
|
||||
|
||||
import (
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
func NewLogger(t testingLogger, ctrl *gomock.Controller) *Logger {
|
||||
return &Logger{
|
||||
MockLogger: NewMockLogger(ctrl),
|
||||
testingLogger: t,
|
||||
}
|
||||
}
|
||||
|
||||
type testingLogger interface {
|
||||
Logf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger provides mock feature but overrides output methods with actual logging.
|
||||
type Logger struct {
|
||||
*MockLogger
|
||||
testingLogger testingLogger
|
||||
}
|
||||
|
||||
func (l *Logger) Printf(format string, v ...interface{}) {
|
||||
l.testingLogger.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
|
||||
l.testingLogger.Logf(format, v...)
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/adaptors (interfaces: Kubeconfig,OIDC,OIDCClient,Env,Logger)
|
||||
|
||||
// Package mock_adaptors is a generated GoMock package.
|
||||
package mock_adaptors
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
adaptors "github.com/int128/kubelogin/adaptors"
|
||||
kubeconfig "github.com/int128/kubelogin/models/kubeconfig"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockKubeconfig is a mock of Kubeconfig interface
|
||||
type MockKubeconfig struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockKubeconfigMockRecorder
|
||||
}
|
||||
|
||||
// MockKubeconfigMockRecorder is the mock recorder for MockKubeconfig
|
||||
type MockKubeconfigMockRecorder struct {
|
||||
mock *MockKubeconfig
|
||||
}
|
||||
|
||||
// NewMockKubeconfig creates a new mock instance
|
||||
func NewMockKubeconfig(ctrl *gomock.Controller) *MockKubeconfig {
|
||||
mock := &MockKubeconfig{ctrl: ctrl}
|
||||
mock.recorder = &MockKubeconfigMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockKubeconfig) EXPECT() *MockKubeconfigMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetCurrentAuthProvider mocks base method
|
||||
func (m *MockKubeconfig) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
|
||||
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
|
||||
func (mr *MockKubeconfigMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// UpdateAuthProvider mocks base method
|
||||
func (m *MockKubeconfig) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
|
||||
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
|
||||
func (mr *MockKubeconfigMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockKubeconfig)(nil).UpdateAuthProvider), arg0)
|
||||
}
|
||||
|
||||
// MockOIDC is a mock of OIDC interface
|
||||
type MockOIDC struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockOIDCMockRecorder
|
||||
}
|
||||
|
||||
// MockOIDCMockRecorder is the mock recorder for MockOIDC
|
||||
type MockOIDCMockRecorder struct {
|
||||
mock *MockOIDC
|
||||
}
|
||||
|
||||
// NewMockOIDC creates a new mock instance
|
||||
func NewMockOIDC(ctrl *gomock.Controller) *MockOIDC {
|
||||
mock := &MockOIDC{ctrl: ctrl}
|
||||
mock.recorder = &MockOIDCMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// New mocks base method
|
||||
func (m *MockOIDC) New(arg0 context.Context, arg1 adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
|
||||
ret := m.ctrl.Call(m, "New", arg0, arg1)
|
||||
ret0, _ := ret[0].(adaptors.OIDCClient)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// New indicates an expected call of New
|
||||
func (mr *MockOIDCMockRecorder) New(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOIDC)(nil).New), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockOIDCClient is a mock of OIDCClient interface
|
||||
type MockOIDCClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockOIDCClientMockRecorder
|
||||
}
|
||||
|
||||
// MockOIDCClientMockRecorder is the mock recorder for MockOIDCClient
|
||||
type MockOIDCClientMockRecorder struct {
|
||||
mock *MockOIDCClient
|
||||
}
|
||||
|
||||
// NewMockOIDCClient creates a new mock instance
|
||||
func NewMockOIDCClient(ctrl *gomock.Controller) *MockOIDCClient {
|
||||
mock := &MockOIDCClient{ctrl: ctrl}
|
||||
mock.recorder = &MockOIDCClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockOIDCClient) EXPECT() *MockOIDCClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AuthenticateByCode mocks base method
|
||||
func (m *MockOIDCClient) AuthenticateByCode(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
ret := m.ctrl.Call(m, "AuthenticateByCode", arg0, arg1)
|
||||
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticateByCode indicates an expected call of AuthenticateByCode
|
||||
func (mr *MockOIDCClientMockRecorder) AuthenticateByCode(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByCode", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByCode), arg0, arg1)
|
||||
}
|
||||
|
||||
// AuthenticateByPassword mocks base method
|
||||
func (m *MockOIDCClient) AuthenticateByPassword(arg0 context.Context, arg1 adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
ret := m.ctrl.Call(m, "AuthenticateByPassword", arg0, arg1)
|
||||
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticateByPassword indicates an expected call of AuthenticateByPassword
|
||||
func (mr *MockOIDCClientMockRecorder) AuthenticateByPassword(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateByPassword", reflect.TypeOf((*MockOIDCClient)(nil).AuthenticateByPassword), arg0, arg1)
|
||||
}
|
||||
|
||||
// Refresh mocks base method
|
||||
func (m *MockOIDCClient) Refresh(arg0 context.Context, arg1 adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
|
||||
ret0, _ := ret[0].(*adaptors.OIDCAuthenticateOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Refresh indicates an expected call of Refresh
|
||||
func (mr *MockOIDCClientMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockOIDCClient)(nil).Refresh), arg0, arg1)
|
||||
}
|
||||
|
||||
// Verify mocks base method
|
||||
func (m *MockOIDCClient) Verify(arg0 context.Context, arg1 adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
|
||||
ret := m.ctrl.Call(m, "Verify", arg0, arg1)
|
||||
ret0, _ := ret[0].(*adaptors.OIDCVerifyOut)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Verify indicates an expected call of Verify
|
||||
func (mr *MockOIDCClientMockRecorder) Verify(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockOIDCClient)(nil).Verify), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockEnv is a mock of Env interface
|
||||
type MockEnv struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockEnvMockRecorder
|
||||
}
|
||||
|
||||
// MockEnvMockRecorder is the mock recorder for MockEnv
|
||||
type MockEnvMockRecorder struct {
|
||||
mock *MockEnv
|
||||
}
|
||||
|
||||
// NewMockEnv creates a new mock instance
|
||||
func NewMockEnv(ctrl *gomock.Controller) *MockEnv {
|
||||
mock := &MockEnv{ctrl: ctrl}
|
||||
mock.recorder = &MockEnvMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockEnv) EXPECT() *MockEnvMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Exec mocks base method
|
||||
func (m *MockEnv) Exec(arg0 context.Context, arg1 string, arg2 []string) (int, error) {
|
||||
ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(int)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Exec indicates an expected call of Exec
|
||||
func (mr *MockEnvMockRecorder) Exec(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockEnv)(nil).Exec), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// ReadPassword mocks base method
|
||||
func (m *MockEnv) ReadPassword(arg0 string) (string, error) {
|
||||
ret := m.ctrl.Call(m, "ReadPassword", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadPassword indicates an expected call of ReadPassword
|
||||
func (mr *MockEnvMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockEnv)(nil).ReadPassword), arg0)
|
||||
}
|
||||
|
||||
// MockLogger is a mock of Logger interface
|
||||
type MockLogger struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockLoggerMockRecorder
|
||||
}
|
||||
|
||||
// MockLoggerMockRecorder is the mock recorder for MockLogger
|
||||
type MockLoggerMockRecorder struct {
|
||||
mock *MockLogger
|
||||
}
|
||||
|
||||
// NewMockLogger creates a new mock instance
|
||||
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
|
||||
mock := &MockLogger{ctrl: ctrl}
|
||||
mock.recorder = &MockLoggerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Debugf mocks base method
|
||||
func (m *MockLogger) Debugf(arg0 adaptors.LogLevel, arg1 string, arg2 ...interface{}) {
|
||||
varargs := []interface{}{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Debugf", varargs...)
|
||||
}
|
||||
|
||||
// Debugf indicates an expected call of Debugf
|
||||
func (mr *MockLoggerMockRecorder) Debugf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
|
||||
}
|
||||
|
||||
// IsEnabled mocks base method
|
||||
func (m *MockLogger) IsEnabled(arg0 adaptors.LogLevel) bool {
|
||||
ret := m.ctrl.Call(m, "IsEnabled", arg0)
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// IsEnabled indicates an expected call of IsEnabled
|
||||
func (mr *MockLoggerMockRecorder) IsEnabled(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockLogger)(nil).IsEnabled), arg0)
|
||||
}
|
||||
|
||||
// Printf mocks base method
|
||||
func (m *MockLogger) Printf(arg0 string, arg1 ...interface{}) {
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "Printf", varargs...)
|
||||
}
|
||||
|
||||
// Printf indicates an expected call of Printf
|
||||
func (mr *MockLoggerMockRecorder) Printf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Printf", reflect.TypeOf((*MockLogger)(nil).Printf), varargs...)
|
||||
}
|
||||
|
||||
// SetLevel mocks base method
|
||||
func (m *MockLogger) SetLevel(arg0 adaptors.LogLevel) {
|
||||
m.ctrl.Call(m, "SetLevel", arg0)
|
||||
}
|
||||
|
||||
// SetLevel indicates an expected call of SetLevel
|
||||
func (mr *MockLoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*MockLogger)(nil).SetLevel), arg0)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
)
|
||||
|
||||
const (
|
||||
logLevelDumpHeaders = 2
|
||||
logLevelDumpBody = 3
|
||||
)
|
||||
|
||||
type Transport struct {
|
||||
Base http.RoundTripper
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if !t.IsDumpEnabled() {
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
|
||||
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
|
||||
return t.Base.RoundTrip(req)
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
|
||||
resp, err := t.Base.RoundTrip(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
|
||||
if err != nil {
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
|
||||
return resp, err
|
||||
}
|
||||
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (t *Transport) IsDumpEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpHeaders)
|
||||
}
|
||||
|
||||
func (t *Transport) IsDumpBodyEnabled() bool {
|
||||
return t.Logger.IsEnabled(logLevelDumpBody)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/mock_adaptors"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
t.req = req
|
||||
return t.resp, nil
|
||||
}
|
||||
|
||||
func TestLoggingTransport_RoundTrip(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(gomock.Any()).
|
||||
Return(true).
|
||||
AnyTimes()
|
||||
|
||||
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
|
||||
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
|
||||
Host: example.com
|
||||
|
||||
dummy`)), req)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a response: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
transport := &Transport{
|
||||
Base: &mockTransport{resp: resp},
|
||||
Logger: logger,
|
||||
}
|
||||
gotResp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Errorf("RoundTrip error: %s", err)
|
||||
}
|
||||
if gotResp != resp {
|
||||
t.Errorf("resp wants %v but %v", resp, gotResp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
|
||||
Return(true)
|
||||
|
||||
transport := &Transport{
|
||||
Logger: logger,
|
||||
}
|
||||
if transport.IsDumpEnabled() != true {
|
||||
t.Errorf("IsDumpEnabled wants true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingTransport_IsDumpBodyEnabled(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
logger := mock_adaptors.NewLogger(t, ctrl)
|
||||
logger.EXPECT().
|
||||
IsEnabled(adaptors.LogLevel(logLevelDumpBody)).
|
||||
Return(true)
|
||||
|
||||
transport := &Transport{
|
||||
Logger: logger,
|
||||
}
|
||||
if transport.IsDumpBodyEnabled() != true {
|
||||
t.Errorf("IsDumpBodyEnabled wants true")
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/logging"
|
||||
"github.com/int128/kubelogin/adaptors/oidc/tls"
|
||||
"github.com/int128/oauth2cli"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for OIDC.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Factory), "*"),
|
||||
wire.Bind(new(adaptors.OIDC), new(*Factory)),
|
||||
)
|
||||
|
||||
type Factory struct {
|
||||
Logger adaptors.Logger
|
||||
}
|
||||
|
||||
// New returns an instance of adaptors.OIDCClient with the given configuration.
|
||||
func (f *Factory) New(ctx context.Context, config adaptors.OIDCClientConfig) (adaptors.OIDCClient, error) {
|
||||
tlsConfig, err := tls.NewConfig(config, f.Logger)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not initialize TLS config: %w", err)
|
||||
}
|
||||
baseTransport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
}
|
||||
loggingTransport := &logging.Transport{
|
||||
Base: baseTransport,
|
||||
Logger: f.Logger,
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: loggingTransport,
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||
provider, err := oidc.NewProvider(ctx, config.Config.IDPIssuerURL)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not discovery the OIDC issuer: %w", err)
|
||||
}
|
||||
return &client{
|
||||
httpClient: httpClient,
|
||||
provider: provider,
|
||||
oauth2Config: oauth2.Config{
|
||||
Endpoint: provider.Endpoint(),
|
||||
ClientID: config.Config.ClientID,
|
||||
ClientSecret: config.Config.ClientSecret,
|
||||
Scopes: append(config.Config.ExtraScopes, oidc.ScopeOpenID),
|
||||
},
|
||||
logger: f.Logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type client struct {
|
||||
httpClient *http.Client
|
||||
provider *oidc.Provider
|
||||
oauth2Config oauth2.Config
|
||||
logger adaptors.Logger
|
||||
}
|
||||
|
||||
// AuthenticateByCode performs the authorization code flow.
|
||||
func (c *client) AuthenticateByCode(ctx context.Context, in adaptors.OIDCAuthenticateByCodeIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
if c.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
|
||||
}
|
||||
nonce, err := newNonce()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not generate a nonce parameter")
|
||||
}
|
||||
config := oauth2cli.Config{
|
||||
OAuth2Config: c.oauth2Config,
|
||||
LocalServerPort: in.LocalServerPort,
|
||||
SkipOpenBrowser: in.SkipOpenBrowser,
|
||||
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline, oidc.Nonce(nonce)},
|
||||
ShowLocalServerURL: in.ShowLocalServerURL.ShowLocalServerURL,
|
||||
}
|
||||
token, err := oauth2cli.GetToken(ctx, config)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not get a token: %w", err)
|
||||
}
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, idToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
|
||||
}
|
||||
if verifiedIDToken.Nonce != nonce {
|
||||
return nil, xerrors.Errorf("nonce of ID token did not match (want %s but was %s)", nonce, verifiedIDToken.Nonce)
|
||||
}
|
||||
claims, err := dumpClaims(verifiedIDToken)
|
||||
if err != nil {
|
||||
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
|
||||
}
|
||||
return &adaptors.OIDCAuthenticateOut{
|
||||
IDToken: idToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
IDTokenExpiry: verifiedIDToken.Expiry,
|
||||
IDTokenClaims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newNonce() (string, error) {
|
||||
var n uint64
|
||||
if err := binary.Read(rand.Reader, binary.LittleEndian, &n); err != nil {
|
||||
return "", xerrors.Errorf("error while reading random: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%x", n), nil
|
||||
}
|
||||
|
||||
// AuthenticateByPassword performs the resource owner password credentials flow.
|
||||
func (c *client) AuthenticateByPassword(ctx context.Context, in adaptors.OIDCAuthenticateByPasswordIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
if c.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
|
||||
}
|
||||
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, in.Username, in.Password)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not get a token: %w", err)
|
||||
}
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, idToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
|
||||
}
|
||||
claims, err := dumpClaims(verifiedIDToken)
|
||||
if err != nil {
|
||||
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
|
||||
}
|
||||
return &adaptors.OIDCAuthenticateOut{
|
||||
IDToken: idToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
IDTokenExpiry: verifiedIDToken.Expiry,
|
||||
IDTokenClaims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify checks client ID and signature of the ID token.
|
||||
// This does not check the expiration and caller should check it.
|
||||
func (c *client) Verify(ctx context.Context, in adaptors.OIDCVerifyIn) (*adaptors.OIDCVerifyOut, error) {
|
||||
if c.httpClient != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.httpClient)
|
||||
}
|
||||
verifier := c.provider.Verifier(&oidc.Config{
|
||||
ClientID: c.oauth2Config.ClientID,
|
||||
SkipExpiryCheck: true,
|
||||
})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
|
||||
}
|
||||
claims, err := dumpClaims(verifiedIDToken)
|
||||
if err != nil {
|
||||
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
|
||||
}
|
||||
return &adaptors.OIDCVerifyOut{
|
||||
IDTokenExpiry: verifiedIDToken.Expiry,
|
||||
IDTokenClaims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Refresh sends a refresh token request and returns a token set.
|
||||
func (c *client) Refresh(ctx context.Context, in adaptors.OIDCRefreshIn) (*adaptors.OIDCAuthenticateOut, error) {
|
||||
currentToken := &oauth2.Token{
|
||||
Expiry: time.Now(),
|
||||
RefreshToken: in.RefreshToken,
|
||||
}
|
||||
source := c.oauth2Config.TokenSource(ctx, currentToken)
|
||||
token, err := source.Token()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not refresh the token: %w", err)
|
||||
}
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
|
||||
}
|
||||
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID})
|
||||
verifiedIDToken, err := verifier.Verify(ctx, idToken)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not verify the id_token: %w", err)
|
||||
}
|
||||
claims, err := dumpClaims(verifiedIDToken)
|
||||
if err != nil {
|
||||
c.logger.Debugf(1, "incomplete claims of the ID token: %w", err)
|
||||
}
|
||||
return &adaptors.OIDCAuthenticateOut{
|
||||
IDToken: idToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
IDTokenExpiry: verifiedIDToken.Expiry,
|
||||
IDTokenClaims: claims,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dumpClaims(token *oidc.IDToken) (map[string]string, error) {
|
||||
var rawClaims map[string]interface{}
|
||||
err := token.Claims(&rawClaims)
|
||||
claims := make(map[string]string)
|
||||
for k, v := range rawClaims {
|
||||
switch v.(type) {
|
||||
case float64:
|
||||
claims[k] = fmt.Sprintf("%f", v.(float64))
|
||||
default:
|
||||
claims[k] = fmt.Sprintf("%s", v)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return claims, xerrors.Errorf("error while decoding the ID token: %w", err)
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// NewConfig returns a tls.Config with the given certificates and options.
|
||||
func NewConfig(config adaptors.OIDCClientConfig, logger adaptors.Logger) (*tls.Config, error) {
|
||||
pool := x509.NewCertPool()
|
||||
if config.Config.IDPCertificateAuthority != "" {
|
||||
logger.Debugf(1, "Loading the certificate %s", config.Config.IDPCertificateAuthority)
|
||||
err := appendCertificateFromFile(pool, config.Config.IDPCertificateAuthority)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority: %w", err)
|
||||
}
|
||||
}
|
||||
if config.Config.IDPCertificateAuthorityData != "" {
|
||||
logger.Debugf(1, "Loading the certificate of idp-certificate-authority-data")
|
||||
err := appendEncodedCertificate(pool, config.Config.IDPCertificateAuthorityData)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not load the certificate of idp-certificate-authority-data: %w", err)
|
||||
}
|
||||
}
|
||||
if config.CACertFilename != "" {
|
||||
logger.Debugf(1, "Loading the certificate %s", config.CACertFilename)
|
||||
err := appendCertificateFromFile(pool, config.CACertFilename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not load the certificate: %w", err)
|
||||
}
|
||||
}
|
||||
c := &tls.Config{
|
||||
InsecureSkipVerify: config.SkipTLSVerify,
|
||||
}
|
||||
if len(pool.Subjects()) > 0 {
|
||||
c.RootCAs = pool
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func appendCertificateFromFile(pool *x509.CertPool, filename string) error {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not read %s: %w", filename, err)
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return xerrors.Errorf("could not append certificate from %s", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendEncodedCertificate(pool *x509.CertPool, base64String string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not decode base64: %w", err)
|
||||
}
|
||||
if !pool.AppendCertsFromPEM(b) {
|
||||
return xerrors.Errorf("could not append certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
)
|
||||
|
||||
func TestNewConfig(t *testing.T) {
|
||||
testingLogger := logger.New(t)
|
||||
testingLogger.SetLevel(1)
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
c, err := NewConfig(adaptors.OIDCClientConfig{}, testingLogger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfig error: %+v", err)
|
||||
}
|
||||
if c.InsecureSkipVerify {
|
||||
t.Errorf("InsecureSkipVerify wants false but true")
|
||||
}
|
||||
if c.RootCAs != nil {
|
||||
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
|
||||
}
|
||||
})
|
||||
t.Run("SkipTLSVerify", func(t *testing.T) {
|
||||
config := adaptors.OIDCClientConfig{
|
||||
SkipTLSVerify: true,
|
||||
}
|
||||
c, err := NewConfig(config, testingLogger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfig error: %+v", err)
|
||||
}
|
||||
if !c.InsecureSkipVerify {
|
||||
t.Errorf("InsecureSkipVerify wants true but false")
|
||||
}
|
||||
if c.RootCAs != nil {
|
||||
t.Errorf("RootCAs wants nil but %+v", c.RootCAs)
|
||||
}
|
||||
})
|
||||
t.Run("AllCertificates", func(t *testing.T) {
|
||||
config := adaptors.OIDCClientConfig{
|
||||
Config: kubeconfig.OIDCConfig{
|
||||
IDPCertificateAuthority: "testdata/ca1.crt",
|
||||
IDPCertificateAuthorityData: string(readFile(t, "testdata/ca2.crt.base64")),
|
||||
},
|
||||
CACertFilename: "testdata/ca3.crt",
|
||||
}
|
||||
c, err := NewConfig(config, testingLogger)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfig error: %+v", err)
|
||||
}
|
||||
if c.InsecureSkipVerify {
|
||||
t.Errorf("InsecureSkipVerify wants false but true")
|
||||
}
|
||||
if c.RootCAs == nil {
|
||||
t.Fatalf("RootCAs wants non-nil but nil")
|
||||
}
|
||||
subjects := c.RootCAs.Subjects()
|
||||
if len(subjects) != 3 {
|
||||
t.Errorf("len(subjects) wants 3 but %d", len(subjects))
|
||||
}
|
||||
})
|
||||
t.Run("InvalidCertificate", func(t *testing.T) {
|
||||
config := adaptors.OIDCClientConfig{
|
||||
Config: kubeconfig.OIDCConfig{
|
||||
IDPCertificateAuthority: "testdata/ca1.crt",
|
||||
IDPCertificateAuthorityData: string(readFile(t, "testdata/ca2.crt.base64")),
|
||||
},
|
||||
CACertFilename: "testdata/Makefile", // invalid cert
|
||||
}
|
||||
_, err := NewConfig(config, testingLogger)
|
||||
if err == nil {
|
||||
t.Fatalf("NewConfig wants non-nil but nil")
|
||||
}
|
||||
t.Logf("expected error: %+v", err)
|
||||
})
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, filename string) []byte {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile error: %s", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
46
di/di.go
46
di/di.go
@@ -1,46 +0,0 @@
|
||||
//+build wireinject
|
||||
|
||||
// Package di provides dependency injection.
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/adaptors/env"
|
||||
"github.com/int128/kubelogin/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
)
|
||||
|
||||
// NewCmd returns an instance of adaptors.Cmd.
|
||||
func NewCmd() adaptors.Cmd {
|
||||
wire.Build(
|
||||
auth.Set,
|
||||
auth.ExtraSet,
|
||||
login.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
oidc.Set,
|
||||
logger.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewCmdWith returns an instance of adaptors.Cmd with given dependencies.
|
||||
// This is only for e2e tests.
|
||||
func NewCmdWith(adaptors.Logger, usecases.LoginShowLocalServerURL) adaptors.Cmd {
|
||||
wire.Build(
|
||||
auth.Set,
|
||||
login.Set,
|
||||
cmd.Set,
|
||||
env.Set,
|
||||
kubeconfig.Set,
|
||||
oidc.Set,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate wire
|
||||
//+build !wireinject
|
||||
|
||||
package di
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors"
|
||||
"github.com/int128/kubelogin/adaptors/cmd"
|
||||
"github.com/int128/kubelogin/adaptors/env"
|
||||
"github.com/int128/kubelogin/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
"github.com/int128/kubelogin/adaptors/oidc"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
"github.com/int128/kubelogin/usecases/auth"
|
||||
"github.com/int128/kubelogin/usecases/login"
|
||||
)
|
||||
|
||||
// Injectors from di.go:
|
||||
|
||||
func NewCmd() adaptors.Cmd {
|
||||
adaptorsLogger := logger.New()
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
envEnv := &env.Env{}
|
||||
showLocalServerURL := &auth.ShowLocalServerURL{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
authentication := &auth.Authentication{
|
||||
OIDC: factory,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
ShowLocalServerURL: showLocalServerURL,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
|
||||
loginLogin := &login.Login{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
|
||||
func NewCmdWith(adaptorsLogger adaptors.Logger, loginShowLocalServerURL usecases.LoginShowLocalServerURL) adaptors.Cmd {
|
||||
factory := &oidc.Factory{
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
envEnv := &env.Env{}
|
||||
authentication := &auth.Authentication{
|
||||
OIDC: factory,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
ShowLocalServerURL: loginShowLocalServerURL,
|
||||
}
|
||||
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{}
|
||||
loginLogin := &login.Login{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
exec := &login.Exec{
|
||||
Authentication: authentication,
|
||||
Kubeconfig: kubeconfigKubeconfig,
|
||||
Env: envEnv,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
cmdCmd := &cmd.Cmd{
|
||||
Login: loginLogin,
|
||||
LoginAndExec: exec,
|
||||
Logger: adaptorsLogger,
|
||||
}
|
||||
return cmdCmd
|
||||
}
|
||||
13
dist/Dockerfile
vendored
Normal file
13
dist/Dockerfile
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM alpine:3.11
|
||||
|
||||
ARG KUBELOGIN_VERSION="{{ env "VERSION" }}"
|
||||
ARG KUBELOGIN_SHA256="{{ sha256 .linux_amd64_archive }}"
|
||||
|
||||
# Download the release and test the checksum
|
||||
RUN wget -O /kubelogin.zip "https://github.com/int128/kubelogin/releases/download/$KUBELOGIN_VERSION/kubelogin_linux_amd64.zip" && \
|
||||
echo "$KUBELOGIN_SHA256 /kubelogin.zip" | sha256sum -c - && \
|
||||
unzip /kubelogin.zip && \
|
||||
rm /kubelogin.zip
|
||||
|
||||
USER daemon
|
||||
ENTRYPOINT ["/kubelogin"]
|
||||
0
kubelogin.rb → dist/kubelogin.rb
vendored
0
kubelogin.rb → dist/kubelogin.rb
vendored
88
dist/oidc-login.yaml
vendored
Normal file
88
dist/oidc-login.yaml
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
apiVersion: krew.googlecontainertools.github.com/v1alpha2
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: oidc-login
|
||||
spec:
|
||||
homepage: https://github.com/int128/kubelogin
|
||||
shortDescription: Log in to the OpenID Connect provider
|
||||
description: |
|
||||
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
|
||||
|
||||
## Credential plugin mode
|
||||
kubectl executes oidc-login before calling the Kubernetes APIs.
|
||||
oidc-login automatically opens the browser and you can log in to the provider.
|
||||
After authentication, kubectl gets the token from oidc-login and you can access the cluster.
|
||||
See https://github.com/int128/kubelogin#credential-plugin-mode for more.
|
||||
|
||||
## Standalone mode
|
||||
Run `kubectl oidc-login`.
|
||||
It automatically opens the browser and you can log in to the provider.
|
||||
After authentication, it writes the token to the kubeconfig and you can access the cluster.
|
||||
See https://github.com/int128/kubelogin#standalone-mode for more.
|
||||
|
||||
caveats: |
|
||||
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
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: .
|
||||
- from: LICENSE
|
||||
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: .
|
||||
- from: LICENSE
|
||||
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: .
|
||||
- from: LICENSE
|
||||
to: .
|
||||
selector:
|
||||
matchLabels:
|
||||
os: windows
|
||||
arch: amd64
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm.zip
|
||||
sha256: "{{ sha256 .linux_arm_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: kubelogin
|
||||
to: .
|
||||
- from: LICENSE
|
||||
to: .
|
||||
selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm
|
||||
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm64.zip
|
||||
sha256: "{{ sha256 .linux_arm64_archive }}"
|
||||
bin: kubelogin
|
||||
files:
|
||||
- from: kubelogin
|
||||
to: .
|
||||
- from: LICENSE
|
||||
to: .
|
||||
selector:
|
||||
matchLabels:
|
||||
os: linux
|
||||
arch: arm64
|
||||
3
docs/acceptance-test-diagram.svg
Normal file
3
docs/acceptance-test-diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/authn.png
BIN
docs/authn.png
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
@@ -1,16 +0,0 @@
|
||||
seqdiag {
|
||||
User -> kubelogin [label = "execute"];
|
||||
kubelogin -> Browser [label = "open"];
|
||||
Browser -> Provider [label = "authentication request"];
|
||||
Browser <-- Provider [label = "redirect"];
|
||||
User -> Browser [label = "enter credentials"];
|
||||
Browser -> Provider [label = "credentials"];
|
||||
Browser <-- Provider [label = "authentication response"];
|
||||
User <-- Browser [label = "success"];
|
||||
kubelogin <-- Browser [label = "close"];
|
||||
kubelogin -> Provider [label = "token request"];
|
||||
kubelogin <-- Provider [label = "token response"];
|
||||
kubelogin -> kubeconfig [label = "write token"];
|
||||
kubelogin <-- kubeconfig;
|
||||
User <-- kubelogin;
|
||||
}
|
||||
3
docs/credential-plugin-diagram.svg
Normal file
3
docs/credential-plugin-diagram.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 30 KiB |
@@ -1,95 +0,0 @@
|
||||
# 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).
|
||||
|
||||
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 KUBECONTEXT \
|
||||
--auth-provider oidc \
|
||||
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
|
||||
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
|
||||
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
## 5. Run kubelogin
|
||||
|
||||
Run `kubelogin`.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```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
|
||||
```
|
||||
111
docs/keycloak.md
111
docs/keycloak.md
@@ -1,111 +0,0 @@
|
||||
# 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 KUBECONTEXT \
|
||||
--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
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-16 22:03:13 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now your `~/.kube/config` should be like:
|
||||
|
||||
```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
|
||||
```
|
||||
223
docs/setup.md
Normal file
223
docs/setup.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Kubernetes OpenID Connection authentication
|
||||
|
||||
This document guides how to set up Kubernetes OpenID Connect (OIDC) authentication.
|
||||
Let's see the following steps:
|
||||
|
||||
1. Set up the OIDC provider
|
||||
1. Verify authentication
|
||||
1. Bind a cluster role
|
||||
1. Set up the Kubernetes API server
|
||||
1. Set up the kubeconfig
|
||||
1. Verify cluster access
|
||||
|
||||
|
||||
## 1. Set up the OIDC provider
|
||||
|
||||
### Google Identity Platform
|
||||
|
||||
You can log in with a Google account.
|
||||
|
||||
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
|
||||
|
||||
- Application Type: Other
|
||||
|
||||
Check the client ID and secret.
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://accounts.google.com`
|
||||
`YOUR_CLIENT_ID` | `xxx.apps.googleusercontent.com`
|
||||
`YOUR_CLIENT_SECRET` | random string
|
||||
|
||||
### Keycloak
|
||||
|
||||
You can log in with a user of Keycloak.
|
||||
Make sure you have an administrator role of the Keycloak realm.
|
||||
|
||||
Open Keycloak and create an OIDC client as follows:
|
||||
|
||||
- Client ID: `YOUR_CLIENT_ID`
|
||||
- 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: `YOUR_CLIENT_ID`
|
||||
- Client Role prefix: `kubernetes:`
|
||||
- Token Claim Name: `groups`
|
||||
- Add to ID token: on
|
||||
|
||||
For example, if you have `admin` role of the client, you will get a JWT with the claim `{"groups": ["kubernetes:admin"]}`.
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://keycloak.example.com/auth/realms/YOUR_REALM`
|
||||
`YOUR_CLIENT_ID` | `YOUR_CLIENT_ID`
|
||||
`YOUR_CLIENT_SECRET` | random string
|
||||
|
||||
### Dex with GitHub
|
||||
|
||||
You can log in with a GitHub account.
|
||||
|
||||
Open [GitHub OAuth Apps](https://github.com/settings/developers) and create an application with the following setting:
|
||||
|
||||
- Application name: (any)
|
||||
- Homepage URL: `https://dex.example.com`
|
||||
- Authorization callback URL: `https://dex.example.com/callback`
|
||||
|
||||
Deploy [Dex](https://github.com/dexidp/dex) with the following config:
|
||||
|
||||
```yaml
|
||||
issuer: https://dex.example.com
|
||||
connectors:
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: YOUR_GITHUB_CLIENT_ID
|
||||
clientSecret: YOUR_GITHUB_CLIENT_SECRET
|
||||
redirectURI: https://dex.example.com/callback
|
||||
staticClients:
|
||||
- id: YOUR_CLIENT_ID
|
||||
name: Kubernetes
|
||||
redirectURIs:
|
||||
- http://localhost:8000
|
||||
- http://localhost:18000
|
||||
secret: YOUR_DEX_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://dex.example.com`
|
||||
`YOUR_CLIENT_ID` | `YOUR_CLIENT_ID`
|
||||
`YOUR_CLIENT_SECRET` | `YOUR_DEX_CLIENT_SECRET`
|
||||
|
||||
### Okta
|
||||
|
||||
You can log in with an Okta user.
|
||||
Okta supports [the authorization code flow with PKCE](https://developer.okta.com/docs/guides/implement-auth-code-pkce/overview/)
|
||||
and this section explains how to set up it.
|
||||
|
||||
Open your Okta organization and create an application with the following options:
|
||||
|
||||
- Application type: Native
|
||||
- Initiate login URI: `http://localhost:8000`
|
||||
- Login redirect URIs:
|
||||
- `http://localhost:8000`
|
||||
- `http://localhost:18000` (used if the port 8000 is already in use)
|
||||
- Allowed grant types: Authorization Code
|
||||
- Client authentication: Use PKCE (for public clients)
|
||||
|
||||
Replace the following variables in the later sections.
|
||||
|
||||
Variable | Value
|
||||
------------------------|------
|
||||
`ISSUER_URL` | `https://YOUR_ORGANIZATION.okta.com`
|
||||
`YOUR_CLIENT_ID` | random string
|
||||
|
||||
You do not need to set `YOUR_CLIENT_SECRET`.
|
||||
|
||||
|
||||
## 2. Verify authentication
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
kubectl oidc-login setup \
|
||||
--oidc-issuer-url=ISSUER_URL \
|
||||
--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
It launches the browser and navigates to `http://localhost:8000`.
|
||||
Please log in to the provider.
|
||||
|
||||
You can set extra options, for example, extra scope or CA certificate.
|
||||
See also the full options.
|
||||
|
||||
```sh
|
||||
kubectl oidc-login setup --help
|
||||
```
|
||||
|
||||
|
||||
## 3. Bind a cluster role
|
||||
|
||||
Here bind `cluster-admin` role to you.
|
||||
|
||||
```sh
|
||||
kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --user='ISSUER_URL#YOUR_SUBJECT'
|
||||
```
|
||||
|
||||
As well as you can create a custom cluster role and bind it.
|
||||
|
||||
|
||||
## 4. Set up the Kubernetes API server
|
||||
|
||||
Add the following flags to kube-apiserver:
|
||||
|
||||
```
|
||||
--oidc-issuer-url=ISSUER_URL
|
||||
--oidc-client-id=YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
See [Kubernetes Authenticating: OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens) for the all flags.
|
||||
|
||||
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
kubeAPIServer:
|
||||
oidcIssuerURL: ISSUER_URL
|
||||
oidcClientID: YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
If you are using [kube-aws](https://github.com/kubernetes-incubator/kube-aws), append the following settings to the `cluster.yaml`:
|
||||
|
||||
```yaml
|
||||
oidc:
|
||||
enabled: true
|
||||
issuerUrl: ISSUER_URL
|
||||
clientId: YOUR_CLIENT_ID
|
||||
```
|
||||
|
||||
|
||||
## 5. Set up the kubeconfig
|
||||
|
||||
Add `oidc` user to the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials oidc \
|
||||
--exec-api-version=client.authentication.k8s.io/v1beta1 \
|
||||
--exec-command=kubectl \
|
||||
--exec-arg=oidc-login \
|
||||
--exec-arg=get-token \
|
||||
--exec-arg=--oidc-issuer-url=ISSUER_URL \
|
||||
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
|
||||
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET
|
||||
```
|
||||
|
||||
|
||||
## 6. Verify cluster access
|
||||
|
||||
Make sure you can access the Kubernetes cluster.
|
||||
|
||||
```sh
|
||||
kubectl --user=oidc cluster-info
|
||||
```
|
||||
|
||||
You can switch the current context to oidc.
|
||||
|
||||
```sh
|
||||
kubectl config set-context --current --user=oidc
|
||||
```
|
||||
|
||||
You can share the kubeconfig to your team members for on-boarding.
|
||||
182
docs/standalone-mode.md
Normal file
182
docs/standalone-mode.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Standalone mode
|
||||
|
||||
You can run kubelogin as a standalone command.
|
||||
In this mode, you need to manually run the command before running kubectl.
|
||||
|
||||
Configure the kubeconfig like:
|
||||
|
||||
```yaml
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: YOUR_CLIENT_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: https://issuer.example.com
|
||||
name: oidc
|
||||
```
|
||||
|
||||
Run kubelogin:
|
||||
|
||||
```sh
|
||||
kubelogin
|
||||
|
||||
# or run as a kubectl plugin
|
||||
kubectl oidc-login
|
||||
```
|
||||
|
||||
It automatically opens the browser and you can log in to the provider.
|
||||
|
||||
<img src="keycloak-login.png" alt="keycloak-login" width="455" height="329">
|
||||
|
||||
After authentication, kubelogin writes the ID token and refresh token to the kubeconfig.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
Open http://localhost:8000 for authentication
|
||||
You got a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
Updated ~/.kubeconfig
|
||||
```
|
||||
|
||||
Now you can access the cluster.
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
|
||||
```
|
||||
|
||||
Your kubeconfig looks like:
|
||||
|
||||
```yaml
|
||||
users:
|
||||
- name: keycloak
|
||||
user:
|
||||
auth-provider:
|
||||
config:
|
||||
client-id: YOUR_CLIENT_ID
|
||||
client-secret: YOUR_CLIENT_SECRET
|
||||
idp-issuer-url: https://issuer.example.com
|
||||
id-token: ey... # kubelogin will add or update the ID token here
|
||||
refresh-token: ey... # kubelogin will add or update the refresh token here
|
||||
name: oidc
|
||||
```
|
||||
|
||||
If the ID token is valid, kubelogin does nothing.
|
||||
|
||||
```
|
||||
% kubelogin
|
||||
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
|
||||
```
|
||||
|
||||
If the ID token has expired, kubelogin will refresh the token using the refresh token in the kubeconfig.
|
||||
If the refresh token has expired, kubelogin will proceed the authentication.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Kubelogin supports the following options:
|
||||
|
||||
```
|
||||
% kubectl oidc-login -h
|
||||
Login to the OpenID Connect provider.
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
Run the following command to show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
|
||||
Usage:
|
||||
main [flags]
|
||||
main [command]
|
||||
|
||||
Available Commands:
|
||||
get-token Run as a kubectl credential plugin
|
||||
help Help about any command
|
||||
setup Show the setup instruction
|
||||
version Print the version information
|
||||
|
||||
Flags:
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
|
||||
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
|
||||
--listen-port ints (Deprecated: use --listen-address)
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--oidc-redirect-url-hostname string Hostname of the redirect URL (default "localhost")
|
||||
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
--add_dir_header If true, adds the file directory to the header
|
||||
--alsologtostderr log to standard error as well as files
|
||||
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
|
||||
--log_dir string If non-empty, write log files in this directory
|
||||
--log_file string If non-empty, use this log file
|
||||
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
|
||||
--logtostderr log to standard error instead of files (default true)
|
||||
--skip_headers If true, avoid header prefixes in the log messages
|
||||
--skip_log_headers If true, avoid headers when opening log files
|
||||
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
|
||||
-v, --v Level number for the log level verbosity
|
||||
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
|
||||
-h, --help help for kubelogin
|
||||
--version version for kubelogin
|
||||
```
|
||||
|
||||
### Kubeconfig
|
||||
|
||||
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
|
||||
It defaults to `~/.kube/config`.
|
||||
|
||||
```sh
|
||||
# by the option
|
||||
kubelogin --kubeconfig /path/to/kubeconfig
|
||||
|
||||
# by the environment variable
|
||||
KUBECONFIG="/path/to/kubeconfig1:/path/to/kubeconfig2" kubelogin
|
||||
```
|
||||
|
||||
If you set multiple files, kubelogin will find the file which has the current authentication (i.e. `user` and `auth-provider`) and write a token to it.
|
||||
|
||||
Kubelogin supports the following keys of `auth-provider` in a kubeconfig.
|
||||
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl) for more.
|
||||
|
||||
Key | Direction | Value
|
||||
----|-----------|------
|
||||
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
|
||||
`client-id` | Read (Mandatory) | Client ID of the provider.
|
||||
`client-secret` | Read (Mandatory) | Client Secret of the provider.
|
||||
`idp-certificate-authority` | Read | CA certificate path of the provider.
|
||||
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
|
||||
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
|
||||
`id-token` | Write | ID token got from the provider.
|
||||
`refresh-token` | Write | Refresh token got from the provider.
|
||||
|
||||
### Extra scopes
|
||||
|
||||
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
|
||||
```
|
||||
|
||||
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
|
||||
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
|
||||
```
|
||||
|
||||
### CA Certificates
|
||||
|
||||
You can use your self-signed certificates for the provider.
|
||||
|
||||
```sh
|
||||
kubectl config set-credentials keycloak \
|
||||
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
|
||||
```
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,403 +0,0 @@
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/e2e_test/idp"
|
||||
"github.com/int128/kubelogin/e2e_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/e2e_test/keys"
|
||||
"github.com/int128/kubelogin/e2e_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/e2e_test/localserver"
|
||||
"github.com/int128/kubelogin/e2e_test/logger"
|
||||
"github.com/int128/kubelogin/usecases"
|
||||
)
|
||||
|
||||
// Run the integration tests.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the kuneconfig.
|
||||
//
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", time.Hour)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().AuthenticatePassword("USER", "PASS", "openid").
|
||||
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--username", "USER", "--password", "PASS")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("env:KUBECONFIG", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{Issuer: serverURL})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "profile groups openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
ExtraScopes: "profile,groups",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACert", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: keys.TLSCACert,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("CACertData", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.StartTLS(t, keys.TLSServerCert, keys.TLSServerKey, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser", "--listen-port", "0")
|
||||
req.wait()
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", time.Hour)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", time.Hour)
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
runCmd(t, ctx, &nopBrowserRequest{t}, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
service := mock_idp.NewMockService(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, service))
|
||||
defer server.Shutdown(t, ctx)
|
||||
var idToken string
|
||||
setupMockIDPForCodeFlow(t, service, serverURL, "openid", &idToken)
|
||||
service.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", -time.Hour), // expired
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
req := startBrowserRequest(t, ctx, nil)
|
||||
runCmd(t, ctx, req, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiration time.Duration) string {
|
||||
t.Helper()
|
||||
var claims struct {
|
||||
jwt.StandardClaims
|
||||
Nonce string `json:"nonce"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
claims.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: issuer,
|
||||
Audience: "kubernetes",
|
||||
Subject: "SUBJECT",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
ExpiresAt: time.Now().Add(expiration).Unix(),
|
||||
}
|
||||
claims.Nonce = nonce
|
||||
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 setupMockIDPForCodeFlow(t *testing.T, service *mock_idp.MockService, serverURL, scope string, idToken *string) {
|
||||
var nonce string
|
||||
service.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
service.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(keys.JWSKeyPair))
|
||||
service.EXPECT().AuthenticateCode(scope, gomock.Any()).
|
||||
DoAndReturn(func(_, gotNonce string) (string, error) {
|
||||
nonce = gotNonce
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
})
|
||||
service.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
*idToken = newIDToken(t, serverURL, nonce, time.Hour)
|
||||
return idp.NewTokenResponse(*idToken, "YOUR_REFRESH_TOKEN"), nil
|
||||
})
|
||||
}
|
||||
|
||||
func runCmd(t *testing.T, ctx context.Context, s usecases.LoginShowLocalServerURL, args ...string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdWith(logger.New(t), s)
|
||||
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
type nopBrowserRequest struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (r *nopBrowserRequest) ShowLocalServerURL(url string) {
|
||||
r.t.Errorf("ShowLocalServerURL must not be called")
|
||||
}
|
||||
|
||||
type browserRequest struct {
|
||||
t *testing.T
|
||||
urlCh chan<- string
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func (r *browserRequest) ShowLocalServerURL(url string) {
|
||||
defer close(r.urlCh)
|
||||
r.t.Logf("Open %s for authentication", url)
|
||||
r.urlCh <- url
|
||||
}
|
||||
|
||||
func (r *browserRequest) wait() {
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) *browserRequest {
|
||||
t.Helper()
|
||||
urlCh := make(chan string)
|
||||
var wg sync.WaitGroup
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case url := <-urlCh:
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
case err := <-ctx.Done():
|
||||
t.Errorf("context done while waiting for URL prompt: %s", err)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
return &browserRequest{t, urlCh, &wg}
|
||||
}
|
||||
|
||||
func setenv(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
t.Fatalf("Could not set the env var %s=%s: %s", key, value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func unsetenv(t *testing.T, key string) {
|
||||
t.Helper()
|
||||
if err := os.Unsetenv(key); err != nil {
|
||||
t.Fatalf("Could not unset the env var %s: %s", key, err)
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/e2e_test/idp (interfaces: Service)
|
||||
|
||||
// Package mock_idp is a generated GoMock package.
|
||||
package mock_idp
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
idp "github.com/int128/kubelogin/e2e_test/idp"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockService is a mock of Service interface
|
||||
type MockService struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockServiceMockRecorder
|
||||
}
|
||||
|
||||
// MockServiceMockRecorder is the mock recorder for MockService
|
||||
type MockServiceMockRecorder struct {
|
||||
mock *MockService
|
||||
}
|
||||
|
||||
// NewMockService creates a new mock instance
|
||||
func NewMockService(ctrl *gomock.Controller) *MockService {
|
||||
mock := &MockService{ctrl: ctrl}
|
||||
mock.recorder = &MockServiceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockService) EXPECT() *MockServiceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AuthenticateCode mocks base method
|
||||
func (m *MockService) AuthenticateCode(arg0, arg1 string) (string, error) {
|
||||
ret := m.ctrl.Call(m, "AuthenticateCode", arg0, arg1)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticateCode indicates an expected call of AuthenticateCode
|
||||
func (mr *MockServiceMockRecorder) AuthenticateCode(arg0, arg1 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockService)(nil).AuthenticateCode), arg0, arg1)
|
||||
}
|
||||
|
||||
// AuthenticatePassword mocks base method
|
||||
func (m *MockService) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
|
||||
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticatePassword indicates an expected call of AuthenticatePassword
|
||||
func (mr *MockServiceMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockService)(nil).AuthenticatePassword), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Discovery mocks base method
|
||||
func (m *MockService) Discovery() *idp.DiscoveryResponse {
|
||||
ret := m.ctrl.Call(m, "Discovery")
|
||||
ret0, _ := ret[0].(*idp.DiscoveryResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Discovery indicates an expected call of Discovery
|
||||
func (mr *MockServiceMockRecorder) Discovery() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockService)(nil).Discovery))
|
||||
}
|
||||
|
||||
// Exchange mocks base method
|
||||
func (m *MockService) Exchange(arg0 string) (*idp.TokenResponse, error) {
|
||||
ret := m.ctrl.Call(m, "Exchange", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Exchange indicates an expected call of Exchange
|
||||
func (mr *MockServiceMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockService)(nil).Exchange), arg0)
|
||||
}
|
||||
|
||||
// GetCertificates mocks base method
|
||||
func (m *MockService) GetCertificates() *idp.CertificatesResponse {
|
||||
ret := m.ctrl.Call(m, "GetCertificates")
|
||||
ret0, _ := ret[0].(*idp.CertificatesResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCertificates indicates an expected call of GetCertificates
|
||||
func (mr *MockServiceMockRecorder) GetCertificates() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockService)(nil).GetCertificates))
|
||||
}
|
||||
|
||||
// Refresh mocks base method
|
||||
func (m *MockService) Refresh(arg0 string) (*idp.TokenResponse, error) {
|
||||
ret := m.ctrl.Call(m, "Refresh", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Refresh indicates an expected call of Refresh
|
||||
func (mr *MockServiceMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockService)(nil).Refresh), arg0)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package keys
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// 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, xerrors.Errorf("could not read JWSKey: %w", err)
|
||||
}
|
||||
block, rest := pem.Decode(b)
|
||||
if block == nil {
|
||||
return nil, xerrors.New("could not decode PEM")
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, xerrors.New("PEM should contain single key but multiple keys")
|
||||
}
|
||||
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not parse the key: %w", err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
4
e2e_test/keys/testdata/.gitignore
vendored
4
e2e_test/keys/testdata/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
/CA
|
||||
*.key
|
||||
*.csr
|
||||
*.crt
|
||||
53
e2e_test/keys/testdata/Makefile
vendored
53
e2e_test/keys/testdata/Makefile
vendored
@@ -1,53 +0,0 @@
|
||||
.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
e2e_test/keys/testdata/openssl.cnf
vendored
37
e2e_test/keys/testdata/openssl.cnf
vendored
@@ -1,37 +0,0 @@
|
||||
[ 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
|
||||
@@ -1,27 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/adaptors/logger"
|
||||
)
|
||||
|
||||
func New(t testingLogger) *logger.Logger {
|
||||
b := &bridge{t}
|
||||
return logger.NewWith(b, b)
|
||||
}
|
||||
|
||||
type testingLogger interface {
|
||||
Logf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
type bridge struct {
|
||||
t testingLogger
|
||||
}
|
||||
|
||||
func (b *bridge) Printf(format string, v ...interface{}) {
|
||||
b.t.Logf(format, v...)
|
||||
}
|
||||
|
||||
func (b *bridge) Output(calldepth int, s string) error {
|
||||
b.t.Logf("%s", s)
|
||||
return nil
|
||||
}
|
||||
46
go.mod
46
go.mod
@@ -1,32 +1,26 @@
|
||||
module github.com/int128/kubelogin
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible
|
||||
github.com/chromedp/chromedp v0.5.3
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/go-test/deep v1.0.1
|
||||
github.com/gogo/protobuf v1.2.1 // indirect
|
||||
github.com/golang/mock v1.3.1
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
|
||||
github.com/google/wire v0.3.0
|
||||
github.com/imdario/mergo v0.3.7 // indirect
|
||||
github.com/int128/oauth2cli v1.4.1
|
||||
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/golang/mock v1.4.3
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/google/wire v0.4.0
|
||||
github.com/int128/oauth2cli v1.11.0
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd
|
||||
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
|
||||
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
|
||||
github.com/spf13/cobra v0.0.7
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
k8s.io/apimachinery v0.18.1
|
||||
k8s.io/client-go v0.18.1
|
||||
k8s.io/klog v1.0.0
|
||||
)
|
||||
|
||||
327
go.sum
327
go.sum
@@ -1,133 +1,338 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac h1:T7V5BXqnYd55Hj/g5uhDYumg9Fp3rMTS6bykYtTIFX4=
|
||||
github.com/chromedp/cdproto v0.0.0-20200116234248-4da64dd111ac/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
|
||||
github.com/chromedp/chromedp v0.5.3 h1:F9LafxmYpsQhWQBdCs+6Sret1zzeeFyHS5LkRF//Ffg=
|
||||
github.com/chromedp/chromedp v0.5.3/go.mod h1:YLdPtndaHQ4rCpSpBG+IPpy9JvX0VD+7aaLxYgYj28w=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
|
||||
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
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/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60=
|
||||
github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
|
||||
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/int128/oauth2cli v1.4.1 h1:IsaYMafEDS1jyArxYdmksw+nMsNxiYCQzdkPj3QF9BY=
|
||||
github.com/int128/oauth2cli v1.4.1/go.mod h1:CMJjyUSgKiobye1M/9byFACOjtB2LRo2mo7boklEKlI=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/int128/listener v1.1.0 h1:2Jb41DWLpkQ3I9bIdBzO8H/tNwMvyl/OBZWtCV5Pjuw=
|
||||
github.com/int128/listener v1.1.0/go.mod h1:68WkmTN8PQtLzc9DucIaagAKeGVyMnyyKIkW4Xn47UA=
|
||||
github.com/int128/oauth2cli v1.11.0 h1:yohafseIxX8xESedQOxB3rpuuodDowYiPaTFMpqPP3Q=
|
||||
github.com/int128/oauth2cli v1.11.0/go.mod h1:O3Tjuj1cyQCuM11KbH2ffh0O6LRX0+O97Z3InsY0M3g=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
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.0/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/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
|
||||
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd h1:sMHc2rZHuzQmrbVoSpt9HgerkXPyIeCSO6k0zUMGfFk=
|
||||
golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190213061140-3a22650c66bd/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/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.18.1 h1:pnHr0LH69kvL29eHldoepUDKTuiOejNZI2A1gaxve3Q=
|
||||
k8s.io/api v0.18.1/go.mod h1:3My4jorQWzSs5a+l7Ge6JBbIxChLnY8HnuT58ZWolss=
|
||||
k8s.io/apimachinery v0.18.1 h1:hKPYcQRPLQoG2e7fKkVl0YCvm9TBefXTfGILa9vjVVk=
|
||||
k8s.io/apimachinery v0.18.1/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA=
|
||||
k8s.io/client-go v0.18.1 h1:2+fnu4LwKJjZVOwijkm1UqZG9aQoFsKEpipOzdfcTD8=
|
||||
k8s.io/client-go v0.18.1/go.mod h1:iCikYRiXOj/yRRFE/aWqrpPtDt4P2JVWhtHkmESTcfY=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
|
||||
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU=
|
||||
k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
|
||||
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
|
||||
392
integration_test/credetial_plugin_test.go
Normal file
392
integration_test/credetial_plugin_test.go
Normal file
@@ -0,0 +1,392 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
"github.com/int128/kubelogin/integration_test/localserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter/mock_credentialpluginwriter"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
)
|
||||
|
||||
// Run the integration tests of the credential plugin use-case.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the output.
|
||||
//
|
||||
func TestCredentialPlugin(t *testing.T) {
|
||||
tokenCacheDir, err := ioutil.TempDir("", "kube")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a cache dir: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(tokenCacheDir); err != nil {
|
||||
t.Errorf("could not clean up the cache dir: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
idpTLS: keypair.None,
|
||||
extraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
t.Run("CertFile", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
tokenCacheKey: tokencache.Key{CACertFilename: keypair.Server.CACertPath},
|
||||
idpTLS: keypair.Server,
|
||||
extraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--certificate-authority", keypair.Server.CACertPath,
|
||||
},
|
||||
})
|
||||
})
|
||||
t.Run("CertData", func(t *testing.T) {
|
||||
testCredentialPlugin(t, credentialPluginTestCase{
|
||||
tokenCacheDir: tokenCacheDir,
|
||||
tokenCacheKey: tokencache.Key{CACertData: keypair.Server.CACertBase64},
|
||||
idpTLS: keypair.Server,
|
||||
extraArgs: []string{
|
||||
"--token-cache-dir", tokenCacheDir,
|
||||
"--certificate-authority-data", keypair.Server.CACertBase64,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type credentialPluginTestCase struct {
|
||||
tokenCacheDir string
|
||||
tokenCacheKey tokencache.Key
|
||||
idpTLS keypair.KeyPair
|
||||
extraArgs []string
|
||||
}
|
||||
|
||||
func testCredentialPlugin(t *testing.T, tc credentialPluginTestCase) {
|
||||
timeout := 1 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &cfg.idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.idpTLS)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &idToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
validIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(validIDToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
})
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &validIDToken)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: validIDToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
provider.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
|
||||
expiredIDToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast)
|
||||
setupTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: expiredIDToken,
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
})
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &cfg.idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.idpTLS)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
assertTokenCache(t, tc, serverURL, tokencache.Value{
|
||||
IDToken: cfg.idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "email profile openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &cfg.idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.idpTLS)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("RedirectURLHostname", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://127.0.0.1:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &cfg.idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.idpTLS)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-redirect-url-hostname", "127.0.0.1",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
|
||||
t.Run("ExtraParams", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), tc.idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
extraParams: map[string]string{
|
||||
"ttl": "86400",
|
||||
"reauth": "false",
|
||||
},
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
writerMock := newCredentialPluginWriterMock(t, ctrl, &cfg.idToken)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, tc.idpTLS)
|
||||
|
||||
args := []string{
|
||||
"--oidc-issuer-url", serverURL,
|
||||
"--oidc-client-id", "kubernetes",
|
||||
"--oidc-auth-request-extra-params", "ttl=86400",
|
||||
"--oidc-auth-request-extra-params", "reauth=false",
|
||||
}
|
||||
args = append(args, tc.extraArgs...)
|
||||
runGetTokenCmd(t, ctx, browserMock, writerMock, args)
|
||||
})
|
||||
}
|
||||
|
||||
func newCredentialPluginWriterMock(t *testing.T, ctrl *gomock.Controller, idToken *string) *mock_credentialpluginwriter.MockInterface {
|
||||
writer := mock_credentialpluginwriter.NewMockInterface(ctrl)
|
||||
writer.EXPECT().
|
||||
Write(gomock.Any()).
|
||||
Do(func(got credentialpluginwriter.Output) {
|
||||
want := credentialpluginwriter.Output{
|
||||
Token: *idToken,
|
||||
Expiry: tokenExpiryFuture,
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
return writer
|
||||
}
|
||||
|
||||
func runGetTokenCmd(t *testing.T, ctx context.Context, b browser.Interface, w credentialpluginwriter.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(logger.New(t), b, w)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin", "get-token",
|
||||
"--v=1",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, v tokencache.Value) {
|
||||
k := tc.tokenCacheKey
|
||||
k.IssuerURL = serverURL
|
||||
k.ClientID = "kubernetes"
|
||||
var r tokencache.Repository
|
||||
err := r.Save(tc.tokenCacheDir, k, v)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertTokenCache(t *testing.T, tc credentialPluginTestCase, serverURL string, want tokencache.Value) {
|
||||
k := tc.tokenCacheKey
|
||||
k.IssuerURL = serverURL
|
||||
k.ClientID = "kubernetes"
|
||||
var r tokencache.Repository
|
||||
got, err := r.FindByKey(tc.tokenCacheDir, k)
|
||||
if err != nil {
|
||||
t.Errorf("could not set up the token cache: %s", err)
|
||||
}
|
||||
if diff := cmp.Diff(&want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
105
integration_test/helpers_test.go
Normal file
105
integration_test/helpers_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenExpiryFuture = time.Now().Add(time.Hour).Round(time.Second)
|
||||
tokenExpiryPast = time.Now().Add(-time.Hour).Round(time.Second)
|
||||
)
|
||||
|
||||
func newIDToken(t *testing.T, issuer, nonce string, expiry time.Time) string {
|
||||
t.Helper()
|
||||
return jwt.EncodeF(t, func(claims *jwt.Claims) {
|
||||
claims.Issuer = issuer
|
||||
claims.Subject = "SUBJECT"
|
||||
claims.IssuedAt = time.Now().Unix()
|
||||
claims.ExpiresAt = expiry.Unix()
|
||||
claims.Audience = []string{"kubernetes", "system"}
|
||||
claims.Nonce = nonce
|
||||
claims.Groups = []string{"admin", "users"}
|
||||
})
|
||||
}
|
||||
|
||||
type authCodeFlowConfig struct {
|
||||
serverURL string
|
||||
scope string
|
||||
redirectURIPrefix string
|
||||
extraParams map[string]string
|
||||
|
||||
// setupAuthCodeFlow will set this after authentication
|
||||
idToken string
|
||||
}
|
||||
|
||||
func setupAuthCodeFlow(t *testing.T, provider *mock_idp.MockProvider, c *authCodeFlowConfig) {
|
||||
var nonce string
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(c.serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
provider.EXPECT().AuthenticateCode(gomock.Any()).
|
||||
DoAndReturn(func(req idp.AuthenticationRequest) (string, error) {
|
||||
if req.Scope != c.scope {
|
||||
t.Errorf("scope wants `%s` but was `%s`", c.scope, req.Scope)
|
||||
}
|
||||
if !strings.HasPrefix(req.RedirectURI, c.redirectURIPrefix) {
|
||||
t.Errorf("redirectURI wants prefix `%s` but was `%s`", c.redirectURIPrefix, req.RedirectURI)
|
||||
}
|
||||
for k, v := range c.extraParams {
|
||||
got := req.RawQuery.Get(k)
|
||||
if got != v {
|
||||
t.Errorf("parameter %s wants `%s` but was `%s`", k, v, got)
|
||||
}
|
||||
}
|
||||
nonce = req.Nonce
|
||||
return "YOUR_AUTH_CODE", nil
|
||||
})
|
||||
provider.EXPECT().Exchange("YOUR_AUTH_CODE").
|
||||
DoAndReturn(func(string) (*idp.TokenResponse, error) {
|
||||
c.idToken = newIDToken(t, c.serverURL, nonce, tokenExpiryFuture)
|
||||
return idp.NewTokenResponse(c.idToken, "YOUR_REFRESH_TOKEN"), nil
|
||||
})
|
||||
}
|
||||
|
||||
func setupROPCFlow(provider *mock_idp.MockProvider, serverURL, scope, username, password, idToken string) {
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
provider.EXPECT().AuthenticatePassword(username, password, scope).
|
||||
Return(idp.NewTokenResponse(idToken, "YOUR_REFRESH_TOKEN"), nil)
|
||||
}
|
||||
|
||||
func newBrowserMock(ctx context.Context, t *testing.T, ctrl *gomock.Controller, k keypair.KeyPair) browser.Interface {
|
||||
b := mock_browser.NewMockInterface(ctrl)
|
||||
b.EXPECT().
|
||||
Open(gomock.Any()).
|
||||
Do(func(url string) {
|
||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: k.TLSConfig}}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
t.Errorf("could not create a request: %s", err)
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("could not send a request: %s", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
return b
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
// Package idp provides a test double of the identity provider of OpenID Connect.
|
||||
package idp
|
||||
|
||||
import (
|
||||
@@ -10,16 +9,16 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func NewHandler(t *testing.T, service Service) *Handler {
|
||||
return &Handler{t, service}
|
||||
func NewHandler(t *testing.T, provider Provider) *Handler {
|
||||
return &Handler{t, provider}
|
||||
}
|
||||
|
||||
// Handler provides a HTTP handler for the identity provider of OpenID Connect.
|
||||
// You need to implement the Service interface.
|
||||
// Handler provides a HTTP handler for the OpenID Connect Provider.
|
||||
// You need to implement the Provider interface.
|
||||
// Note that this skips some security checks and is only for testing.
|
||||
type Handler struct {
|
||||
t *testing.T
|
||||
service Service
|
||||
t *testing.T
|
||||
provider Provider
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -58,14 +57,14 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
p := r.URL.Path
|
||||
switch {
|
||||
case m == "GET" && p == "/.well-known/openid-configuration":
|
||||
discoveryResponse := h.service.Discovery()
|
||||
discoveryResponse := h.provider.Discovery()
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w)
|
||||
if err := e.Encode(discoveryResponse); err != nil {
|
||||
return xerrors.Errorf("could not render json: %w", err)
|
||||
}
|
||||
case m == "GET" && p == "/certs":
|
||||
certificatesResponse := h.service.GetCertificates()
|
||||
certificatesResponse := h.provider.GetCertificates()
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
e := json.NewEncoder(w)
|
||||
if err := e.Encode(certificatesResponse); err != nil {
|
||||
@@ -75,8 +74,14 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 3.1.2.1. Authentication Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||
q := r.URL.Query()
|
||||
redirectURI, scope, state, nonce := q.Get("redirect_uri"), q.Get("scope"), q.Get("state"), q.Get("nonce")
|
||||
code, err := h.service.AuthenticateCode(scope, nonce)
|
||||
redirectURI, state := q.Get("redirect_uri"), q.Get("state")
|
||||
code, err := h.provider.AuthenticateCode(AuthenticationRequest{
|
||||
RedirectURI: redirectURI,
|
||||
State: state,
|
||||
Scope: q.Get("scope"),
|
||||
Nonce: q.Get("nonce"),
|
||||
RawQuery: q,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
@@ -92,7 +97,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 3.1.3.1. Token Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
|
||||
code := r.Form.Get("code")
|
||||
tokenResponse, err := h.service.Exchange(code)
|
||||
tokenResponse, err := h.provider.Exchange(code)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("token request error: %w", err)
|
||||
}
|
||||
@@ -105,7 +110,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 4.3. Resource Owner Password Credentials Grant
|
||||
// https://tools.ietf.org/html/rfc6749#section-4.3
|
||||
username, password, scope := r.Form.Get("username"), r.Form.Get("password"), r.Form.Get("scope")
|
||||
tokenResponse, err := h.service.AuthenticatePassword(username, password, scope)
|
||||
tokenResponse, err := h.provider.AuthenticatePassword(username, password, scope)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("authentication error: %w", err)
|
||||
}
|
||||
@@ -118,7 +123,7 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
// 12.1. Refresh Request
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
|
||||
refreshToken := r.Form.Get("refresh_token")
|
||||
tokenResponse, err := h.service.Refresh(refreshToken)
|
||||
tokenResponse, err := h.provider.Refresh(refreshToken)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("token refresh error: %w", err)
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
// Package idp provides a test double of an OpenID Connect Provider.
|
||||
package idp
|
||||
|
||||
//go:generate mockgen -destination mock_idp/mock_service.go github.com/int128/kubelogin/e2e_test/idp Service
|
||||
//go:generate mockgen -destination mock_idp/mock_idp.go github.com/int128/kubelogin/integration_test/idp Provider
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Service provides discovery and authentication methods.
|
||||
// Provider provides discovery and authentication methods.
|
||||
// If an implemented method returns an ErrorResponse,
|
||||
// the handler will respond 400 and corresponding json of the ErrorResponse.
|
||||
// Otherwise, the handler will respond 500 and fail the current test.
|
||||
type Service interface {
|
||||
type Provider interface {
|
||||
Discovery() *DiscoveryResponse
|
||||
GetCertificates() *CertificatesResponse
|
||||
AuthenticateCode(scope, nonce string) (code string, err error)
|
||||
AuthenticateCode(req AuthenticationRequest) (code string, err error)
|
||||
Exchange(code string) (*TokenResponse, error)
|
||||
AuthenticatePassword(username, password, scope string) (*TokenResponse, error)
|
||||
Refresh(refreshToken string) (*TokenResponse, error)
|
||||
@@ -88,6 +90,14 @@ func NewCertificatesResponse(idTokenKeyPair *rsa.PrivateKey) *CertificatesRespon
|
||||
}
|
||||
}
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
RedirectURI string
|
||||
State string
|
||||
Scope string // space separated string
|
||||
Nonce string
|
||||
RawQuery url.Values
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
122
integration_test/idp/mock_idp/mock_idp.go
Normal file
122
integration_test/idp/mock_idp/mock_idp.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/integration_test/idp (interfaces: Provider)
|
||||
|
||||
// Package mock_idp is a generated GoMock package.
|
||||
package mock_idp
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
idp "github.com/int128/kubelogin/integration_test/idp"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockProvider is a mock of Provider interface
|
||||
type MockProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockProviderMockRecorder is the mock recorder for MockProvider
|
||||
type MockProviderMockRecorder struct {
|
||||
mock *MockProvider
|
||||
}
|
||||
|
||||
// NewMockProvider creates a new mock instance
|
||||
func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
|
||||
mock := &MockProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockProvider) EXPECT() *MockProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AuthenticateCode mocks base method
|
||||
func (m *MockProvider) AuthenticateCode(arg0 idp.AuthenticationRequest) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticateCode", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticateCode indicates an expected call of AuthenticateCode
|
||||
func (mr *MockProviderMockRecorder) AuthenticateCode(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticateCode", reflect.TypeOf((*MockProvider)(nil).AuthenticateCode), arg0)
|
||||
}
|
||||
|
||||
// AuthenticatePassword mocks base method
|
||||
func (m *MockProvider) AuthenticatePassword(arg0, arg1, arg2 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AuthenticatePassword", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// AuthenticatePassword indicates an expected call of AuthenticatePassword
|
||||
func (mr *MockProviderMockRecorder) AuthenticatePassword(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthenticatePassword", reflect.TypeOf((*MockProvider)(nil).AuthenticatePassword), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Discovery mocks base method
|
||||
func (m *MockProvider) Discovery() *idp.DiscoveryResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Discovery")
|
||||
ret0, _ := ret[0].(*idp.DiscoveryResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Discovery indicates an expected call of Discovery
|
||||
func (mr *MockProviderMockRecorder) Discovery() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discovery", reflect.TypeOf((*MockProvider)(nil).Discovery))
|
||||
}
|
||||
|
||||
// Exchange mocks base method
|
||||
func (m *MockProvider) Exchange(arg0 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Exchange", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Exchange indicates an expected call of Exchange
|
||||
func (mr *MockProviderMockRecorder) Exchange(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exchange", reflect.TypeOf((*MockProvider)(nil).Exchange), arg0)
|
||||
}
|
||||
|
||||
// GetCertificates mocks base method
|
||||
func (m *MockProvider) GetCertificates() *idp.CertificatesResponse {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCertificates")
|
||||
ret0, _ := ret[0].(*idp.CertificatesResponse)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetCertificates indicates an expected call of GetCertificates
|
||||
func (mr *MockProviderMockRecorder) GetCertificates() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCertificates", reflect.TypeOf((*MockProvider)(nil).GetCertificates))
|
||||
}
|
||||
|
||||
// Refresh mocks base method
|
||||
func (m *MockProvider) Refresh(arg0 string) (*idp.TokenResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Refresh", arg0)
|
||||
ret0, _ := ret[0].(*idp.TokenResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Refresh indicates an expected call of Refresh
|
||||
func (mr *MockProviderMockRecorder) Refresh(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockProvider)(nil).Refresh), arg0)
|
||||
}
|
||||
62
integration_test/keypair/keypair.go
Normal file
62
integration_test/keypair/keypair.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package keypair
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// KeyPair represents a pair of certificate and key.
|
||||
type KeyPair struct {
|
||||
CertPath string
|
||||
KeyPath string
|
||||
CACertPath string
|
||||
CACertBase64 string
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// None represents non-TLS.
|
||||
var None KeyPair
|
||||
|
||||
// Server is a KeyPair for TLS server.
|
||||
// These files should be generated by Makefile before test.
|
||||
var Server = KeyPair{
|
||||
CertPath: "keypair/testdata/server.crt",
|
||||
KeyPath: "keypair/testdata/server.key",
|
||||
CACertPath: "keypair/testdata/ca.crt",
|
||||
CACertBase64: readAsBase64("keypair/testdata/ca.crt"),
|
||||
TLSConfig: newTLSConfig("keypair/testdata/ca.crt"),
|
||||
}
|
||||
|
||||
func readAsBase64(name string) string {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
var s strings.Builder
|
||||
e := base64.NewEncoder(base64.StdEncoding, &s)
|
||||
if _, err := io.Copy(e, f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := e.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func newTLSConfig(name string) *tls.Config {
|
||||
b, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p := x509.NewCertPool()
|
||||
if !p.AppendCertsFromPEM(b) {
|
||||
panic("could not append the CA cert")
|
||||
}
|
||||
return &tls.Config{RootCAs: p}
|
||||
}
|
||||
26
integration_test/keypair/testdata/Makefile
vendored
Normal file
26
integration_test/keypair/testdata/Makefile
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
EXPIRY := 3650
|
||||
OUTPUT_DIR := testdata
|
||||
TARGETS := ca.key
|
||||
TARGETS += ca.crt
|
||||
TARGETS += server.key
|
||||
TARGETS += server.crt
|
||||
|
||||
.PHONY: all
|
||||
all: $(TARGETS)
|
||||
|
||||
ca.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
ca.csr: ca.key
|
||||
openssl req -new -key ca.key -out $@ -subj "/CN=hello-ca" -config openssl.cnf
|
||||
ca.crt: ca.key ca.csr
|
||||
openssl x509 -req -in ca.csr -signkey ca.key -out $@ -days $(EXPIRY)
|
||||
server.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
server.csr: openssl.cnf server.key
|
||||
openssl req -new -key server.key -out $@ -subj "/CN=localhost" -config openssl.cnf
|
||||
server.crt: openssl.cnf server.csr ca.crt ca.key
|
||||
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out $@ -sha256 -days $(EXPIRY) -extensions v3_req -extfile openssl.cnf
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -v $(TARGETS)
|
||||
17
integration_test/keypair/testdata/ca.crt
vendored
Normal file
17
integration_test/keypair/testdata/ca.crt
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICojCCAYoCCQCNsdXicWqF2DANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAho
|
||||
ZWxsby1jYTAeFw0yMDAyMDYwMTMzMzNaFw0zMDAyMDMwMTMzMzNaMBMxETAPBgNV
|
||||
BAMMCGhlbGxvLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxcPN
|
||||
dS88B6ewqVn/m9yO74OLIwNrqMci8l7olP9XlcJhxUZs+3WZQpsSj5nC4yEx8uPQ
|
||||
bTtBKnXXVDe+8k7OsLTruu9+isTaYk4o/TZbuw/N31ZAiT0pJw8hdypTQyMLbeDr
|
||||
Vl4bbrfbYywx30DyrHxUkgzOWs459Uwc1wWu0W7M21GY4KENHFE3OAcD58FMvvrh
|
||||
vgkslATwwW4M2UtXUFJ8XHh26g/J450DU2gwNxpcSdIsvFE6zSyAxU55RElph7mE
|
||||
ru9cNWAYhCRZvlZQ2VlH7C6JQ3SHyA9RZmBbPpXhtl9zavFkGx2MEwDp/3FmUukR
|
||||
yJnS2KnAo0QdBeS1LQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAXGfoDwuKY4TyF
|
||||
fhKg553Y5I6VDCDc97jyW9yyfc0kvPjQc4EGQV+1eQqMSeh2THpgEEKk9hH/g35a
|
||||
grPRcBTsEWpbQd16yWyulQeyOtPeWZB2FvAigMaAdMmeXlTs6++gJ6PjPuACa2Jl
|
||||
nJ/AjCqKFxkn0yEVkPTY0c/I9A12xhCmATqIrQiK1pPowiFxQb4M8Cm0z0AkaJZr
|
||||
iW0NCOJlLzBqRpFquL4umNaIsxTmOshfM70NpQGRjKREBuK6S0qWsRR0wz4b9Rvi
|
||||
62qW4zU94q2EDIoCItjHP4twGENXJDC0vLCsKfA5AvbPzszd5/4ifYe2C00Rn7/O
|
||||
lIxrspMm
|
||||
-----END CERTIFICATE-----
|
||||
17
integration_test/keypair/testdata/ca.csr
vendored
Normal file
17
integration_test/keypair/testdata/ca.csr
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICrDCCAZQCAQAwEzERMA8GA1UEAwwIaGVsbG8tY2EwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQDFw811LzwHp7CpWf+b3I7vg4sjA2uoxyLyXuiU/1eV
|
||||
wmHFRmz7dZlCmxKPmcLjITHy49BtO0EqdddUN77yTs6wtOu6736KxNpiTij9Nlu7
|
||||
D83fVkCJPSknDyF3KlNDIwtt4OtWXhtut9tjLDHfQPKsfFSSDM5azjn1TBzXBa7R
|
||||
bszbUZjgoQ0cUTc4BwPnwUy++uG+CSyUBPDBbgzZS1dQUnxceHbqD8njnQNTaDA3
|
||||
GlxJ0iy8UTrNLIDFTnlESWmHuYSu71w1YBiEJFm+VlDZWUfsLolDdIfID1FmYFs+
|
||||
leG2X3Nq8WQbHYwTAOn/cWZS6RHImdLYqcCjRB0F5LUtAgMBAAGgVDBSBgkqhkiG
|
||||
9w0BCQ4xRTBDMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMBQGA1UdEQQNMAuCCWxv
|
||||
Y2FsaG9zdDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
o/Uthp3NWx0ydTn/GBZI+vA3gI7Qd9UwLvwkeinldRlPM9DOXpG6LR0C40f6u+fj
|
||||
mXvUDerveYJI8rpo+Ds0UVqy63AH/zZLG7M96L5Nv2KnK40bkfVNez858Yqp1u17
|
||||
/ci1ZsQIElU5v2qKozaHdQThDVtD5ZZdZoQwLvBLE/Dwpe/4VZZFh8smPMR+Mhcq
|
||||
+b7gpSy1RiUffk0ZMjuF9Nc9OODdQMTCf+86i0qWXGVzkhfHKAGv+xarHEztcmxF
|
||||
GUgUYW8DMvYBjEzaGRM1n0aIFkQO6y8SUXvGMIGBSMC4jfBH3ghIXg1+nD6Uah4D
|
||||
16r+CLjFsDUdn/DK/fIQVw==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
27
integration_test/keypair/testdata/ca.key
vendored
Normal file
27
integration_test/keypair/testdata/ca.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAxcPNdS88B6ewqVn/m9yO74OLIwNrqMci8l7olP9XlcJhxUZs
|
||||
+3WZQpsSj5nC4yEx8uPQbTtBKnXXVDe+8k7OsLTruu9+isTaYk4o/TZbuw/N31ZA
|
||||
iT0pJw8hdypTQyMLbeDrVl4bbrfbYywx30DyrHxUkgzOWs459Uwc1wWu0W7M21GY
|
||||
4KENHFE3OAcD58FMvvrhvgkslATwwW4M2UtXUFJ8XHh26g/J450DU2gwNxpcSdIs
|
||||
vFE6zSyAxU55RElph7mEru9cNWAYhCRZvlZQ2VlH7C6JQ3SHyA9RZmBbPpXhtl9z
|
||||
avFkGx2MEwDp/3FmUukRyJnS2KnAo0QdBeS1LQIDAQABAoIBAHSoWtMsaMnPNlu/
|
||||
thM32K0auIGP6/rkdQ3pxGLX+M9jmY7oSzNOHHj4xssklZyroS45CmLU2Ez2tG1+
|
||||
cMm4iR4dqwxbaBbtpjDlEDLF1PiUiwmadHlANb1PpJsJwZHR41UOn2QUITR/ig+H
|
||||
K2gZhM0QjkaU/Uj9a5zyJ/UC6iupmgCtj8ij65B49qKMODxV4gqZstRSZiJ3gb6R
|
||||
TBeR3PUWQS62MZueEQz2eF0eXkXKsFWcbLfHArjfIJ533zW68A0vabgqOhCwJTks
|
||||
+rkyaKUjEwJJQcgpCfUI4t9HAwICqYtw0fQdDDaMak2XTDFnGHn1/VfSi818U5Sa
|
||||
jZ+/uIECgYEA4uCd5ISsLWebSv2r5f97N8+WcW0MSJ0XP5MniiMvlaNOtJmoN3H0
|
||||
PlgR3uwpH9TNWHITZEEb/I93r2E3f24v2018g0uJuwiRDusxHs7yuxw/93Y5rwtG
|
||||
3twL1k0GyeLCxfM2y2QQU/awXISx7nNk2f0umvTWmjrEw632oQSWCSECgYEA3yaF
|
||||
zDk7k1u1GdZAh+pQAyUtjzHSvQjiE2JzLaH8BTorACrczRascX5yyMi0QfLYnoYt
|
||||
UL9dCb4Z8HUclj55kBSx8anvVtf3XAT3hZ3sm8LV3JLGDeURGS8rGdV+tyFk7zw9
|
||||
XgvaLj3xjB5CoJvtOhbXvqF9M9yp4TMBb5El7o0CgYBTx5Bm15thNPZCrgQxbbOJ
|
||||
u42ZmyRDGEeCgYvDVhT3VBP3WxqkRt9juk/3GwxgpcuikpWYmvaDwFL5H5RH6V+g
|
||||
wy9sqJNWzuYKNU2xS8iU0ezJLA5HFon4OBfi7hTIroUwZgzg9LWW2+zqbVHrdQ9T
|
||||
9Eumiy1ITNVmUTJW6YOiIQKBgBE4BsEAdZFkVTAeMTKLqQrlFoPjI1DE27UFNsAB
|
||||
rNG2cFT9+bW1ly7WxAKsQgSIuaBZ2CtP6Nz0l0nPr5oEThsJDcYJB9faqFKoa3Ua
|
||||
/4PxX9E6Xh/6WfxogFno+HMnF4PCUTXtkjNZQkc+moOMJJ0D4DfsfB3BXDZtWiIC
|
||||
wDuNAoGBAN5ZFTXwE1VpWxIWq5dV9+0aVOP/eB+h7Pt+u2xyRip31FGcpGEzGniu
|
||||
VmOzWApW7NmvD0QWtstJWlCpsw+rOptAL0oVunlB70KuYuA+2Q5g/Cw3kUrvXqbe
|
||||
mkCoV21eFAmpU+I6z/n7pUDXPuUsmTkzmwedv0HUuNSQJmHZOMPD
|
||||
-----END RSA PRIVATE KEY-----
|
||||
1
integration_test/keypair/testdata/ca.srl
vendored
Normal file
1
integration_test/keypair/testdata/ca.srl
vendored
Normal file
@@ -0,0 +1 @@
|
||||
9E12C7A1AF348811
|
||||
14
integration_test/keypair/testdata/openssl.cnf
vendored
Normal file
14
integration_test/keypair/testdata/openssl.cnf
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
|
||||
[ req_distinguished_name ]
|
||||
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
subjectAltName = @alt_names
|
||||
extendedKeyUsage = serverAuth
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = localhost
|
||||
18
integration_test/keypair/testdata/server.crt
vendored
Normal file
18
integration_test/keypair/testdata/server.crt
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC7zCCAdegAwIBAgIJAJ4Sx6GvNIgRMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
|
||||
BAMMCGhlbGxvLWNhMB4XDTIwMDIwNjAxMzMzM1oXDTMwMDIwMzAxMzMzM1owFDES
|
||||
MBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F7yVyphLtJr+/AxI8k5Er
|
||||
MFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4TKyVaU0XK41ZSbDAchbW
|
||||
349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTkC3gSVF92+TzrrlbkZ5+W
|
||||
YVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDgkGuwwhzXfVUpxJ/DYLXZ
|
||||
yHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV+2ovP0Nggn6XJNh/g1QK
|
||||
o4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABo0UwQzAJBgNVHRMEAjAAMAsGA1UdDwQE
|
||||
AwIF4DAUBgNVHREEDTALgglsb2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
|
||||
DQYJKoZIhvcNAQELBQADggEBACqlq8b7trNRtKUm1PbY7dnrAFCOnV4OT2R98s17
|
||||
Q6tmCXM1DvQ101W0ih/lh6iPyU4JM2A0kvO+gizuL/Dmvb6oh+3ox0mMLposptso
|
||||
gCE1K3SXlvlcLdM6hXRJ5+XwlSCHM6o2Y4yABnKjT6Zr+CMh86a5abDx33hkJ4QR
|
||||
6I+/iBHVLiCVv0wUF3jD/T+HxinEQrB4cQsSgKmfPClrc8n7rkWWE+MdwEL4VZCH
|
||||
ufabw178aYibVvJ8k3rushjLlftkDyCNno2rz8YWrnaxabVR0EqSPInyYQenmv2r
|
||||
/CedVKpmdH2RV8ubkwqa0s6cmpRkyu6FS2g3LviJhmm0nwQ=
|
||||
-----END CERTIFICATE-----
|
||||
17
integration_test/keypair/testdata/server.csr
vendored
Normal file
17
integration_test/keypair/testdata/server.csr
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIICrTCCAZUCAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F
|
||||
7yVyphLtJr+/AxI8k5ErMFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4
|
||||
TKyVaU0XK41ZSbDAchbW349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTk
|
||||
C3gSVF92+TzrrlbkZ5+WYVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDg
|
||||
kGuwwhzXfVUpxJ/DYLXZyHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV
|
||||
+2ovP0Nggn6XJNh/g1QKo4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABoFQwUgYJKoZI
|
||||
hvcNAQkOMUUwQzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAUBgNVHREEDTALggls
|
||||
b2NhbGhvc3QwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEB
|
||||
ACUQ0j3XuUZY3fso5tEgOGVjVsTU+C4pIsEw3e1KyIB93i56ANKaq1uBLtnlWtYi
|
||||
R4RxZ3Sf08GzoEHAHpWoQ4BQ3WGDQjxSdezbudWMuNnNyvyhkh36tmmp/PLA4iZD
|
||||
Q/d1odzGWg1HMqY0/Q3hfz40MQ9IEBBm+5zKw3tLsKNIKdSdlY7Ul3Z9PUsqsOVW
|
||||
uF0LKsTMEh1CpbYnOBS2EQjComVM5kYfdQwDNh+BMok8rH7mHFYRmrwYUrU9njsM
|
||||
eoKqhLkoSu6hw1Cgd9Yru5lC541KfxsSN4Cj6rkm+Qv/5zjvIPxYZ+akSQQR9hR3
|
||||
2O9THHKqaQS0xmD+y8NjfYk=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
27
integration_test/keypair/testdata/server.key
vendored
Normal file
27
integration_test/keypair/testdata/server.key
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA3CJvDbbiiY/o9lgzeABLCtZe56h2w9Dzejy02M6F7yVyphLt
|
||||
Jr+/AxI8k5ErMFRitkzgIUkLn/90CZjPI3k5OnLtUAJ4XLj4gUGjziy4TKyVaU0X
|
||||
K41ZSbDAchbW349lsEgW5ZnL4qNaZeCvvbYec+RroVc6ZXCcDp4BATTkC3gSVF92
|
||||
+TzrrlbkZ5+WYVeoMpAPWDPq2zwQx4RcYlpkpI/fzLezRqjRcZJx3FDgkGuwwhzX
|
||||
fVUpxJ/DYLXZyHCKyaT7e8YIs8e7TRekOwrLCfssfhJSdWopf6aYRZFV+2ovP0Ng
|
||||
gn6XJNh/g1QKo4wzJAf6v5WMc0jEvb7EuSG+nwIDAQABAoIBAEYvWFb4C0wurOj2
|
||||
ABrvhP2Ekaesh4kxMp+zgTlqxzsTJnWarS/gjKcPBm9KJon3La3P3tnd7y3pBXcV
|
||||
2F0IBl4DTHRpBTUS6HBVnENc8LnJgK2dHZkOLPyYtRLrA0Et+A73PQ2hNmchC+5V
|
||||
b9K9oQH0Pvim1gCHocnrSIi480hQPazO9/gnHvFtu9Tdsx9kM9jgChhh5VaR+nzM
|
||||
uw6MpUSzCri3/W6K5Hz8F1lLQcZ6o3vyyFtLPjFrWT4J1UiHqAoxuCGeWTvMbGQl
|
||||
9Cg0SMX4FLBpbIoMonhM3hb9Cw3FVRGU/1i4oF+gEIGlX/v9CPT5HAZmD59rawc8
|
||||
11x7yTECgYEA8EYVGKauLHeI1nJdCoVrWI73W1wtwt6prJbzltpoAR4tb3Qu6gBX
|
||||
JQNd+Ifhl28lK6lPnCGk/SsmfiKSp/XE4IyS1GBd2LpxWj5R50GePCg4hAzk9Xyx
|
||||
M9SJAFQw73pODgu4RWFXTCOMo9crahJ6X/O+MckHqbFqqohJ8nChGDkCgYEA6oro
|
||||
Ql/ymO7UdYCay7OlzKeg8ud6XgkZ+wkpSG/5TQ0QZWVN3aSOLztEdVfh3PAqWob0
|
||||
KgVhLmq6CYwq+HSzU92bvFqCgYUMU8tYzeRboGLxyEdAGL+EW0j8wQyZKYR5cNz4
|
||||
yM0hpM6kbJ0zZqkYVM/XfRT2RTGwTMakF/FOHZcCgYEAmf4guTriuIcoAWEstniK
|
||||
Myj16ezrO1Df6Eia+B0kuUqxDhSlmL39HDDLQmU8NYU7in8qEcQSbVwBgKgB3HoM
|
||||
42nVFR5qJ2RfD9qPPar1klKo3iExgRCYtcJKyBYtgt6dNi1WvcjEXX0PP1bBcWtE
|
||||
WUjrphbUvXKDDabp1eNPrCkCgYBOw/F2APTeyS4Oe+8AQ8eFcDIMARLGK7ZO6Oe1
|
||||
TO1jI+UCuD+rFI0vbW7zHV1brkf6+OFcj0vwo6TweeMgZ0il/IFFgvva9UyLg3nC
|
||||
Q1NGDJR4Fv1+kiqn4V4IkuuI1tVVws/F16XZzA/J7g0KB/WE3fvXJMgDuskjL36C
|
||||
D+aU5wKBgQCvEg+mJYny1QiR/mQuowX34xf4CkMl7Xq9YDH7W/3AlwuPrPNHaZjh
|
||||
SvSCz9I8vV0E4ur6atazgCblnvA/G3d4r8YYx+e1l30WJHMgoZRxxHMH74tmUPAj
|
||||
Klic16BJikSQclMeFdlJqf2UHd37eEuYnxorpep8YGP7/eN1ghHWAw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
)
|
||||
|
||||
type Shutdowner interface {
|
||||
@@ -28,7 +30,15 @@ func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
|
||||
}
|
||||
|
||||
// Start starts an authentication server.
|
||||
func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
|
||||
// If k is non-nil, it starts a TLS server.
|
||||
func Start(t *testing.T, h http.Handler, k keypair.KeyPair) (string, Shutdowner) {
|
||||
if k == keypair.None {
|
||||
return startNoTLS(t, h)
|
||||
}
|
||||
return startTLS(t, h, k)
|
||||
}
|
||||
|
||||
func startNoTLS(t *testing.T, h http.Handler) (string, Shutdowner) {
|
||||
t.Helper()
|
||||
l, port := newLocalhostListener(t)
|
||||
url := "http://localhost:" + port
|
||||
@@ -44,8 +54,7 @@ func Start(t *testing.T, h http.Handler) (string, Shutdowner) {
|
||||
return url, &shutdowner{l, s}
|
||||
}
|
||||
|
||||
// Start starts an authentication server with TLS.
|
||||
func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Shutdowner) {
|
||||
func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) (string, Shutdowner) {
|
||||
t.Helper()
|
||||
l, port := newLocalhostListener(t)
|
||||
url := "https://localhost:" + port
|
||||
@@ -53,7 +62,7 @@ func StartTLS(t *testing.T, cert string, key string, h http.Handler) (string, Sh
|
||||
Handler: h,
|
||||
}
|
||||
go func() {
|
||||
err := s.ServeTLS(l, cert, key)
|
||||
err := s.ServeTLS(l, k.CertPath, k.KeyPath)
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
t.Error(err)
|
||||
}
|
||||
307
integration_test/standalone_test.go
Normal file
307
integration_test/standalone_test.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/integration_test/idp"
|
||||
"github.com/int128/kubelogin/integration_test/idp/mock_idp"
|
||||
"github.com/int128/kubelogin/integration_test/keypair"
|
||||
"github.com/int128/kubelogin/integration_test/kubeconfig"
|
||||
"github.com/int128/kubelogin/integration_test/localserver"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/browser/mock_browser"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
"github.com/int128/kubelogin/pkg/testing/jwt"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
)
|
||||
|
||||
// Run the integration tests of the Login use-case.
|
||||
//
|
||||
// 1. Start the auth server.
|
||||
// 2. Run the Cmd.
|
||||
// 3. Open a request for the local server.
|
||||
// 4. Verify the kubeconfig.
|
||||
//
|
||||
func TestStandalone(t *testing.T) {
|
||||
t.Run("NoTLS", func(t *testing.T) {
|
||||
testStandalone(t, keypair.None)
|
||||
})
|
||||
t.Run("TLS", func(t *testing.T) {
|
||||
testStandalone(t, keypair.Server)
|
||||
})
|
||||
}
|
||||
|
||||
func testStandalone(t *testing.T, idpTLS keypair.KeyPair) {
|
||||
timeout := 5 * time.Second
|
||||
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: cfg.idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
idToken := newIDToken(t, serverURL, "", tokenExpiryFuture)
|
||||
setupROPCFlow(provider, serverURL, "openid", "USER", "PASS", idToken)
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasValidRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
idToken := newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryFuture)
|
||||
provider.EXPECT().Discovery().Return(idp.NewDiscoveryResponse(serverURL))
|
||||
provider.EXPECT().GetCertificates().Return(idp.NewCertificatesResponse(jwt.PrivateKey))
|
||||
provider.EXPECT().Refresh("VALID_REFRESH_TOKEN").
|
||||
Return(idp.NewTokenResponse(idToken, "NEW_REFRESH_TOKEN"), nil)
|
||||
browserMock := mock_browser.NewMockInterface(ctrl)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "VALID_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: idToken,
|
||||
RefreshToken: "NEW_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("HasExpiredRefreshToken", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
provider.EXPECT().Refresh("EXPIRED_REFRESH_TOKEN").
|
||||
Return(nil, &idp.ErrorResponse{Code: "invalid_request", Description: "token has expired"}).
|
||||
MaxTimes(2) // package oauth2 will retry refreshing the token
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDToken: newIDToken(t, serverURL, "YOUR_NONCE", tokenExpiryPast), // expired
|
||||
RefreshToken: "EXPIRED_REFRESH_TOKEN",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: cfg.idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("env_KUBECONFIG", func(t *testing.T) {
|
||||
// do not run this in parallel due to change of the env var
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
|
||||
defer unsetenv(t, "KUBECONFIG")
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: cfg.idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ExtraScopes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
provider := mock_idp.NewMockProvider(ctrl)
|
||||
serverURL, server := localserver.Start(t, idp.NewHandler(t, provider), idpTLS)
|
||||
defer server.Shutdown(t, ctx)
|
||||
cfg := authCodeFlowConfig{
|
||||
serverURL: serverURL,
|
||||
scope: "profile groups openid",
|
||||
redirectURIPrefix: "http://localhost:",
|
||||
}
|
||||
setupAuthCodeFlow(t, provider, &cfg)
|
||||
browserMock := newBrowserMock(ctx, t, ctrl, idpTLS)
|
||||
|
||||
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
|
||||
Issuer: serverURL,
|
||||
ExtraScopes: "profile,groups",
|
||||
IDPCertificateAuthority: idpTLS.CACertPath,
|
||||
})
|
||||
defer os.Remove(kubeConfigFilename)
|
||||
|
||||
args := []string{
|
||||
"--kubeconfig", kubeConfigFilename,
|
||||
}
|
||||
runRootCmd(t, ctx, browserMock, args)
|
||||
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
|
||||
IDToken: cfg.idToken,
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func runRootCmd(t *testing.T, ctx context.Context, b browser.Interface, args []string) {
|
||||
t.Helper()
|
||||
cmd := di.NewCmdForHeadless(logger.New(t), b, nil)
|
||||
exitCode := cmd.Run(ctx, append([]string{
|
||||
"kubelogin",
|
||||
"--v=1",
|
||||
"--listen-address", "127.0.0.1:0",
|
||||
}, args...), "HEAD")
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exit status wants 0 but %d", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
2
main.go
2
main.go
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/int128/kubelogin/di"
|
||||
"github.com/int128/kubelogin/pkg/di"
|
||||
)
|
||||
|
||||
var version = "HEAD"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package kubeconfig
|
||||
|
||||
// ContextName represents name of a context.
|
||||
type ContextName string
|
||||
|
||||
// UserName represents name of a user.
|
||||
type UserName string
|
||||
|
||||
// AuthProvider represents the authentication provider,
|
||||
// i.e. context, user and auth-provider in a kubeconfig.
|
||||
type AuthProvider struct {
|
||||
LocationOfOrigin string // Path to the kubeconfig file which contains the user
|
||||
UserName UserName // User name
|
||||
ContextName ContextName // Context name (optional)
|
||||
OIDCConfig OIDCConfig
|
||||
}
|
||||
|
||||
// OIDCConfig represents a configuration of an OIDC provider.
|
||||
type OIDCConfig struct {
|
||||
IDPIssuerURL string // idp-issuer-url
|
||||
ClientID string // client-id
|
||||
ClientSecret string // client-secret
|
||||
IDPCertificateAuthority string // (optional) idp-certificate-authority
|
||||
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
|
||||
ExtraScopes []string // (optional) extra-scopes
|
||||
IDToken string // (optional) id-token
|
||||
RefreshToken string // (optional) refresh-token
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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
|
||||
33
pkg/adaptors/browser/browser.go
Normal file
33
pkg/adaptors/browser/browser.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/pkg/browser"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_browser/mock_browser.go github.com/int128/kubelogin/pkg/adaptors/browser Interface
|
||||
|
||||
func init() {
|
||||
// In credential plugin mode, some browser launcher writes a message to stdout
|
||||
// and it may break the credential json for client-go.
|
||||
// This prevents the browser launcher from breaking the credential json.
|
||||
browser.Stdout = os.Stderr
|
||||
}
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Browser)),
|
||||
wire.Bind(new(Interface), new(*Browser)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Open(url string) error
|
||||
}
|
||||
|
||||
type Browser struct{}
|
||||
|
||||
// Open opens the default browser.
|
||||
func (*Browser) Open(url string) error {
|
||||
return browser.OpenURL(url)
|
||||
}
|
||||
47
pkg/adaptors/browser/mock_browser/mock_browser.go
Normal file
47
pkg/adaptors/browser/mock_browser/mock_browser.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/browser (interfaces: Interface)
|
||||
|
||||
// Package mock_browser is a generated GoMock package.
|
||||
package mock_browser
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Open mocks base method
|
||||
func (m *MockInterface) Open(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Open", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Open indicates an expected call of Open
|
||||
func (mr *MockInterfaceMockRecorder) Open(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockInterface)(nil).Open), arg0)
|
||||
}
|
||||
71
pkg/adaptors/certpool/cert.go
Normal file
71
pkg/adaptors/certpool/cert.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Package certpool provides loading certificates from files or base64 encoded string.
|
||||
package certpool
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/google/wire"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool Interface
|
||||
|
||||
// Set provides an implementation and interface.
|
||||
var Set = wire.NewSet(
|
||||
wire.Value(NewFunc(New)),
|
||||
wire.Struct(new(CertPool), "*"),
|
||||
wire.Bind(new(Interface), new(*CertPool)),
|
||||
)
|
||||
|
||||
type NewFunc func() Interface
|
||||
|
||||
// New returns an instance which implements the Interface.
|
||||
func New() Interface {
|
||||
return &CertPool{pool: x509.NewCertPool()}
|
||||
}
|
||||
|
||||
type Interface interface {
|
||||
AddFile(filename string) error
|
||||
AddBase64Encoded(s string) error
|
||||
SetRootCAs(cfg *tls.Config)
|
||||
}
|
||||
|
||||
// CertPool represents a pool of certificates.
|
||||
type CertPool struct {
|
||||
pool *x509.CertPool
|
||||
}
|
||||
|
||||
// SetRootCAs sets cfg.RootCAs if it has any certificate.
|
||||
// Otherwise do nothing.
|
||||
func (p *CertPool) SetRootCAs(cfg *tls.Config) {
|
||||
if len(p.pool.Subjects()) > 0 {
|
||||
cfg.RootCAs = p.pool
|
||||
}
|
||||
}
|
||||
|
||||
// AddFile loads the certificate from the file.
|
||||
func (p *CertPool) AddFile(filename string) error {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not read %s: %w", filename, err)
|
||||
}
|
||||
if !p.pool.AppendCertsFromPEM(b) {
|
||||
return xerrors.Errorf("could not append certificate from %s", filename)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddBase64Encoded loads the certificate from the base64 encoded string.
|
||||
func (p *CertPool) AddBase64Encoded(s string) error {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("could not decode base64: %w", err)
|
||||
}
|
||||
if !p.pool.AppendCertsFromPEM(b) {
|
||||
return xerrors.Errorf("could not append certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
58
pkg/adaptors/certpool/cert_test.go
Normal file
58
pkg/adaptors/certpool/cert_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package certpool
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCertPool_AddFile(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
p := New()
|
||||
if err := p.AddFile("testdata/ca1.crt"); err != nil {
|
||||
t.Errorf("AddFile error: %s", err)
|
||||
}
|
||||
var cfg tls.Config
|
||||
p.SetRootCAs(&cfg)
|
||||
if n := len(cfg.RootCAs.Subjects()); n != 1 {
|
||||
t.Errorf("n wants 1 but was %d", n)
|
||||
}
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
p := New()
|
||||
err := p.AddFile("testdata/Makefile")
|
||||
if err == nil {
|
||||
t.Errorf("AddFile wants an error but was nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCertPool_AddBase64Encoded(t *testing.T) {
|
||||
p := New()
|
||||
if err := p.AddBase64Encoded(readFile(t, "testdata/ca2.crt.base64")); err != nil {
|
||||
t.Errorf("AddBase64Encoded error: %s", err)
|
||||
}
|
||||
var cfg tls.Config
|
||||
p.SetRootCAs(&cfg)
|
||||
if n := len(cfg.RootCAs.Subjects()); n != 1 {
|
||||
t.Errorf("n wants 1 but was %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertPool_SetRootCAs(t *testing.T) {
|
||||
p := New()
|
||||
var cfg tls.Config
|
||||
p.SetRootCAs(&cfg)
|
||||
if cfg.RootCAs != nil {
|
||||
t.Errorf("cfg.RootCAs wants nil but was %+v", cfg.RootCAs)
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(t *testing.T, filename string) string {
|
||||
t.Helper()
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile error: %s", err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
74
pkg/adaptors/certpool/mock_certpool/mock_certpool.go
Normal file
74
pkg/adaptors/certpool/mock_certpool/mock_certpool.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/certpool (interfaces: Interface)
|
||||
|
||||
// Package mock_certpool is a generated GoMock package.
|
||||
package mock_certpool
|
||||
|
||||
import (
|
||||
tls "crypto/tls"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AddBase64Encoded mocks base method
|
||||
func (m *MockInterface) AddBase64Encoded(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddBase64Encoded", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddBase64Encoded indicates an expected call of AddBase64Encoded
|
||||
func (mr *MockInterfaceMockRecorder) AddBase64Encoded(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBase64Encoded", reflect.TypeOf((*MockInterface)(nil).AddBase64Encoded), arg0)
|
||||
}
|
||||
|
||||
// AddFile mocks base method
|
||||
func (m *MockInterface) AddFile(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AddFile", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AddFile indicates an expected call of AddFile
|
||||
func (mr *MockInterfaceMockRecorder) AddFile(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFile", reflect.TypeOf((*MockInterface)(nil).AddFile), arg0)
|
||||
}
|
||||
|
||||
// SetRootCAs mocks base method
|
||||
func (m *MockInterface) SetRootCAs(arg0 *tls.Config) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetRootCAs", arg0)
|
||||
}
|
||||
|
||||
// SetRootCAs indicates an expected call of SetRootCAs
|
||||
func (mr *MockInterfaceMockRecorder) SetRootCAs(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRootCAs", reflect.TypeOf((*MockInterface)(nil).SetRootCAs), arg0)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
.PHONY: clean
|
||||
|
||||
all: ca1.crt ca1.crt.base64 ca2.crt ca2.crt.base64 ca3.crt ca3.crt.base64
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -v *.key *.csr *.crt *.base64
|
||||
-rm -v *.key *.csr *.crt *.base64
|
||||
|
||||
%.key:
|
||||
openssl genrsa -out $@ 1024
|
||||
24
pkg/adaptors/clock/clock.go
Normal file
24
pkg/adaptors/clock/clock.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Package clock provides the system clock.
|
||||
package clock
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Clock), "*"),
|
||||
wire.Bind(new(Interface), new(*Clock)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the current time.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
68
pkg/adaptors/cmd/cmd.go
Normal file
68
pkg/adaptors/cmd/cmd.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
)
|
||||
|
||||
// Set provides an implementation and interface for Cmd.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Cmd), "*"),
|
||||
wire.Bind(new(Interface), new(*Cmd)),
|
||||
wire.Struct(new(Root), "*"),
|
||||
wire.Struct(new(GetToken), "*"),
|
||||
wire.Struct(new(Setup), "*"),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Run(ctx context.Context, args []string, version string) int
|
||||
}
|
||||
|
||||
var defaultListenAddress = []string{"127.0.0.1:8000", "127.0.0.1:18000"}
|
||||
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
|
||||
|
||||
// Cmd provides interaction with command line interface (CLI).
|
||||
type Cmd struct {
|
||||
Root *Root
|
||||
GetToken *GetToken
|
||||
Setup *Setup
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
// Run parses the command line arguments and executes the specified use-case.
|
||||
// It returns an exit code, that is 0 on success or 1 on error.
|
||||
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
|
||||
rootCmd := cmd.Root.New()
|
||||
rootCmd.Version = version
|
||||
rootCmd.SilenceUsage = true
|
||||
rootCmd.SilenceErrors = true
|
||||
|
||||
getTokenCmd := cmd.GetToken.New()
|
||||
rootCmd.AddCommand(getTokenCmd)
|
||||
|
||||
setupCmd := cmd.Setup.New()
|
||||
rootCmd.AddCommand(setupCmd)
|
||||
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(*cobra.Command, []string) {
|
||||
cmd.Logger.Printf("kubelogin version %s (%s %s_%s)", version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
rootCmd.SetArgs(args[1:])
|
||||
if err := rootCmd.ExecuteContext(ctx); err != nil {
|
||||
cmd.Logger.Printf("error: %s", err)
|
||||
cmd.Logger.V(1).Infof("stacktrace: %+v", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
374
pkg/adaptors/cmd/cmd_test.go
Normal file
374
pkg/adaptors/cmd/cmd_test.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/int128/kubelogin/pkg/testing/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone/mock_standalone"
|
||||
)
|
||||
|
||||
func TestCmd_Run(t *testing.T) {
|
||||
const executable = "kubelogin"
|
||||
const version = "HEAD"
|
||||
|
||||
t.Run("root", func(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
in standalone.Input
|
||||
}{
|
||||
"Defaults": {
|
||||
args: []string{executable},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: defaultListenAddress,
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"when --listen-port is set, it should convert the port to address": {
|
||||
args: []string{
|
||||
executable,
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"when --listen-port is set, it should ignore --listen-address flags": {
|
||||
args: []string{
|
||||
executable,
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--listen-address", "127.0.0.1:30080",
|
||||
"--listen-address", "127.0.0.1:40080",
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"FullOptions": {
|
||||
args: []string{executable,
|
||||
"--kubeconfig", "/path/to/kubeconfig",
|
||||
"--context", "hello.k8s.local",
|
||||
"--user", "google",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--grant-type", "authcode",
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: standalone.Input{
|
||||
KubeconfigFilename: "/path/to/kubeconfig",
|
||||
KubeconfigContext: "hello.k8s.local",
|
||||
KubeconfigUser: "google",
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=authcode-keyboard": {
|
||||
args: []string{executable,
|
||||
"--grant-type", "authcode-keyboard",
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=password": {
|
||||
args: []string{executable,
|
||||
"--grant-type", "password",
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=auto": {
|
||||
args: []string{executable,
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: standalone.Input{
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, c := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
mockStandalone := mock_standalone.NewMockInterface(ctrl)
|
||||
mockStandalone.EXPECT().
|
||||
Do(ctx, c.in)
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Standalone: mockStandalone,
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, c.args, 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{
|
||||
Root: &Root{
|
||||
Standalone: mock_standalone.NewMockInterface(ctrl),
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("get-token", func(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
in credentialplugin.Input
|
||||
}{
|
||||
"Defaults": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
|
||||
RedirectURLHostname: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"FullOptions": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--certificate-authority-data", "BASE64ENCODED",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--grant-type", "authcode",
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--skip-open-browser",
|
||||
"--oidc-auth-request-extra-params", "ttl=86400",
|
||||
"--oidc-auth-request-extra-params", "reauth=true",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
CACertFilename: "/path/to/cacert",
|
||||
CACertData: "BASE64ENCODED",
|
||||
SkipTLSVerify: true,
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
RedirectURLHostname: "localhost",
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=authcode-keyboard": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--grant-type", "authcode-keyboard",
|
||||
"--oidc-auth-request-extra-params", "ttl=86400",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{
|
||||
AuthRequestExtraParams: map[string]string{"ttl": "86400"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=password": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--grant-type", "password",
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=auto": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--listen-address", "127.0.0.1:10080",
|
||||
"--listen-address", "127.0.0.1:20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
GrantOptionSet: authentication.GrantOptionSet{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, c := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
getToken := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, c.in)
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, c.args, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("MissingMandatoryOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
|
||||
Logger: logger.New(t),
|
||||
},
|
||||
Logger: logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
|
||||
if exitCode != 1 {
|
||||
t.Errorf("exitCode wants 1 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
83
pkg/adaptors/cmd/get_token.go
Normal file
83
pkg/adaptors/cmd/get_token.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// getTokenOptions represents the options for get-token command.
|
||||
type getTokenOptions struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
TokenCacheDir string
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
func (o *getTokenOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded data for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
|
||||
o.authenticationOptions.register(f)
|
||||
}
|
||||
|
||||
type GetToken struct {
|
||||
GetToken credentialplugin.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (cmd *GetToken) New() *cobra.Command {
|
||||
var o getTokenOptions
|
||||
c := &cobra.Command{
|
||||
Use: "get-token [flags]",
|
||||
Short: "Run as a kubectl credential plugin",
|
||||
Args: func(c *cobra.Command, args []string) error {
|
||||
if err := cobra.NoArgs(c, args); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.IssuerURL == "" {
|
||||
return xerrors.New("--oidc-issuer-url is missing")
|
||||
}
|
||||
if o.ClientID == "" {
|
||||
return xerrors.New("--oidc-client-id is missing")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get-token: %w", err)
|
||||
}
|
||||
in := credentialplugin.Input{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CACertFilename,
|
||||
CACertData: o.CACertData,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
TokenCacheDir: o.TokenCacheDir,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
if err := cmd.GetToken.Do(c.Context(), in); err != nil {
|
||||
return xerrors.Errorf("get-token: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
149
pkg/adaptors/cmd/root.go
Normal file
149
pkg/adaptors/cmd/root.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
"github.com/int128/kubelogin/pkg/usecases/authentication"
|
||||
"github.com/int128/kubelogin/pkg/usecases/standalone"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const longDescription = `Login to the OpenID Connect provider.
|
||||
|
||||
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
|
||||
Run the following command to show the setup instruction:
|
||||
|
||||
kubectl oidc-login setup
|
||||
|
||||
See https://github.com/int128/kubelogin for more.
|
||||
`
|
||||
|
||||
// rootOptions represents the options for the root command.
|
||||
type rootOptions struct {
|
||||
Kubeconfig string
|
||||
Context string
|
||||
User string
|
||||
CertificateAuthority string
|
||||
SkipTLSVerify bool
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
func (o *rootOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
|
||||
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
|
||||
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
|
||||
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
o.authenticationOptions.register(f)
|
||||
}
|
||||
|
||||
type authenticationOptions struct {
|
||||
GrantType string
|
||||
ListenAddress []string
|
||||
ListenPort []int // deprecated
|
||||
SkipOpenBrowser bool
|
||||
RedirectURLHostname string
|
||||
AuthRequestExtraParams map[string]string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// determineListenAddress returns the addresses from the flags.
|
||||
// Note that --listen-address is always given due to the default value.
|
||||
// If --listen-port is not set, it returns --listen-address.
|
||||
// If --listen-port is set, it returns the strings of --listen-port.
|
||||
func (o *authenticationOptions) determineListenAddress() []string {
|
||||
if len(o.ListenPort) == 0 {
|
||||
return o.ListenAddress
|
||||
}
|
||||
var a []string
|
||||
for _, p := range o.ListenPort {
|
||||
a = append(a, fmt.Sprintf("127.0.0.1:%d", p))
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
var allGrantType = strings.Join([]string{
|
||||
"auto",
|
||||
"authcode",
|
||||
"authcode-keyboard",
|
||||
"password",
|
||||
}, "|")
|
||||
|
||||
func (o *authenticationOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("The authorization grant type to use. One of (%s)", allGrantType))
|
||||
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "Address to bind to the local server. If multiple addresses are given, it will try binding in order")
|
||||
//TODO: remove the deprecated flag
|
||||
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "(Deprecated: use --listen-address)")
|
||||
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
|
||||
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "Hostname of the redirect URL")
|
||||
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "Extra query parameters to send with an authentication request")
|
||||
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
|
||||
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
|
||||
}
|
||||
|
||||
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
|
||||
switch {
|
||||
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
|
||||
s.AuthCodeOption = &authentication.AuthCodeOption{
|
||||
BindAddress: o.determineListenAddress(),
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
RedirectURLHostname: o.RedirectURLHostname,
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
}
|
||||
case o.GrantType == "authcode-keyboard":
|
||||
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{
|
||||
AuthRequestExtraParams: o.AuthRequestExtraParams,
|
||||
}
|
||||
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
|
||||
s.ROPCOption = &authentication.ROPCOption{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
default:
|
||||
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Standalone standalone.Interface
|
||||
Logger logger.Interface
|
||||
}
|
||||
|
||||
func (cmd *Root) New() *cobra.Command {
|
||||
var o rootOptions
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "kubelogin",
|
||||
Short: "Login to the OpenID Connect provider",
|
||||
Long: longDescription,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("invalid option: %w", err)
|
||||
}
|
||||
in := standalone.Input{
|
||||
KubeconfigFilename: o.Kubeconfig,
|
||||
KubeconfigContext: kubeconfig.ContextName(o.Context),
|
||||
KubeconfigUser: kubeconfig.UserName(o.User),
|
||||
CACertFilename: o.CertificateAuthority,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
|
||||
return xerrors.Errorf("login: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(rootCmd.Flags())
|
||||
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
|
||||
return rootCmd
|
||||
}
|
||||
74
pkg/adaptors/cmd/setup.go
Normal file
74
pkg/adaptors/cmd/setup.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/int128/kubelogin/pkg/usecases/setup"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// setupOptions represents the options for setup command.
|
||||
type setupOptions struct {
|
||||
IssuerURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
ExtraScopes []string
|
||||
CACertFilename string
|
||||
CACertData string
|
||||
SkipTLSVerify bool
|
||||
authenticationOptions authenticationOptions
|
||||
}
|
||||
|
||||
func (o *setupOptions) register(f *pflag.FlagSet) {
|
||||
f.SortFlags = false
|
||||
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider")
|
||||
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
|
||||
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
|
||||
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
|
||||
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
|
||||
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded data 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")
|
||||
o.authenticationOptions.register(f)
|
||||
}
|
||||
|
||||
type Setup struct {
|
||||
Setup setup.Interface
|
||||
}
|
||||
|
||||
func (cmd *Setup) New() *cobra.Command {
|
||||
var o setupOptions
|
||||
c := &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Show the setup instruction",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("setup: %w", err)
|
||||
}
|
||||
in := setup.Stage2Input{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
ClientSecret: o.ClientSecret,
|
||||
ExtraScopes: o.ExtraScopes,
|
||||
CACertFilename: o.CACertFilename,
|
||||
CACertData: o.CACertData,
|
||||
SkipTLSVerify: o.SkipTLSVerify,
|
||||
GrantOptionSet: grantOptionSet,
|
||||
}
|
||||
if c.Flags().Lookup("listen-address").Changed {
|
||||
in.ListenAddressArgs = o.authenticationOptions.ListenAddress
|
||||
}
|
||||
if in.IssuerURL == "" || in.ClientID == "" {
|
||||
cmd.Setup.DoStage1()
|
||||
return nil
|
||||
}
|
||||
if err := cmd.Setup.DoStage2(c.Context(), in); err != nil {
|
||||
return xerrors.Errorf("setup: %w", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
o.register(c.Flags())
|
||||
return c
|
||||
}
|
||||
51
pkg/adaptors/credentialpluginwriter/credential_plugin.go
Normal file
51
pkg/adaptors/credentialpluginwriter/credential_plugin.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Package credentialpluginwriter provides a writer for a credential plugin.
|
||||
package credentialpluginwriter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/wire"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_credentialpluginwriter/mock_credentialpluginwriter.go github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter Interface
|
||||
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Writer), "*"),
|
||||
wire.Bind(new(Interface), new(*Writer)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Write(out Output) error
|
||||
}
|
||||
|
||||
// Output represents an output object of the credential plugin.
|
||||
type Output struct {
|
||||
Token string
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type Writer struct{}
|
||||
|
||||
// Write writes the ExecCredential to standard output for kubectl.
|
||||
func (*Writer) Write(out Output) error {
|
||||
ec := &v1beta1.ExecCredential{
|
||||
TypeMeta: v1.TypeMeta{
|
||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||
Kind: "ExecCredential",
|
||||
},
|
||||
Status: &v1beta1.ExecCredentialStatus{
|
||||
Token: out.Token,
|
||||
ExpirationTimestamp: &v1.Time{Time: out.Expiry},
|
||||
},
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
if err := e.Encode(ec); err != nil {
|
||||
return xerrors.Errorf("could not write the ExecCredential: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter (interfaces: Interface)
|
||||
|
||||
// Package mock_credentialpluginwriter is a generated GoMock package.
|
||||
package mock_credentialpluginwriter
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
credentialpluginwriter "github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Write mocks base method
|
||||
func (m *MockInterface) Write(arg0 credentialpluginwriter.Output) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Write", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Write indicates an expected call of Write
|
||||
func (mr *MockInterfaceMockRecorder) Write(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockInterface)(nil).Write), arg0)
|
||||
}
|
||||
45
pkg/adaptors/kubeconfig/kubeconfig.go
Normal file
45
pkg/adaptors/kubeconfig/kubeconfig.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package kubeconfig
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
"github.com/int128/kubelogin/pkg/adaptors/logger"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination mock_kubeconfig/mock_kubeconfig.go github.com/int128/kubelogin/pkg/adaptors/kubeconfig Interface
|
||||
|
||||
// Set provides an implementation and interface for Kubeconfig.
|
||||
var Set = wire.NewSet(
|
||||
wire.Struct(new(Kubeconfig), "*"),
|
||||
wire.Bind(new(Interface), new(*Kubeconfig)),
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error)
|
||||
UpdateAuthProvider(auth *AuthProvider) error
|
||||
}
|
||||
|
||||
// ContextName represents name of a context.
|
||||
type ContextName string
|
||||
|
||||
// UserName represents name of a user.
|
||||
type UserName string
|
||||
|
||||
// AuthProvider represents the authentication provider,
|
||||
// i.e. context, user and auth-provider in a kubeconfig.
|
||||
type AuthProvider struct {
|
||||
LocationOfOrigin string // Path to the kubeconfig file which contains the user
|
||||
UserName UserName // User name
|
||||
ContextName ContextName // (optional) Context name
|
||||
IDPIssuerURL string // idp-issuer-url
|
||||
ClientID string // client-id
|
||||
ClientSecret string // (optional) client-secret
|
||||
IDPCertificateAuthority string // (optional) idp-certificate-authority
|
||||
IDPCertificateAuthorityData string // (optional) idp-certificate-authority-data
|
||||
ExtraScopes []string // (optional) extra-scopes
|
||||
IDToken string // (optional) id-token
|
||||
RefreshToken string // (optional) refresh-token
|
||||
}
|
||||
|
||||
type Kubeconfig struct {
|
||||
Logger logger.Interface
|
||||
}
|
||||
@@ -3,16 +3,15 @@ package kubeconfig
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"golang.org/x/xerrors"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
|
||||
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error) {
|
||||
config, err := loadByDefaultRules(explicitFilename)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not load kubeconfig: %w", err)
|
||||
return nil, xerrors.Errorf("could not load the kubeconfig: %w", err)
|
||||
}
|
||||
auth, err := findCurrentAuthProvider(config, contextName, userName)
|
||||
if err != nil {
|
||||
@@ -26,7 +25,7 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
|
||||
rules.ExplicitPath = explicitFilename
|
||||
config, err := rules.Load()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("error while loading config: %w", err)
|
||||
return nil, xerrors.Errorf("load error: %w", err)
|
||||
}
|
||||
return config, err
|
||||
}
|
||||
@@ -35,16 +34,16 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
|
||||
// If contextName is given, this returns the user of the context.
|
||||
// If userName is given, this ignores the context and returns the user.
|
||||
// If any context or user is not found, this returns an error.
|
||||
func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
|
||||
func findCurrentAuthProvider(config *api.Config, contextName ContextName, userName UserName) (*AuthProvider, error) {
|
||||
if userName == "" {
|
||||
if contextName == "" {
|
||||
contextName = kubeconfig.ContextName(config.CurrentContext)
|
||||
contextName = ContextName(config.CurrentContext)
|
||||
}
|
||||
contextNode, ok := config.Contexts[string(contextName)]
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("context %s does not exist", contextName)
|
||||
}
|
||||
userName = kubeconfig.UserName(contextNode.AuthInfo)
|
||||
userName = UserName(contextNode.AuthInfo)
|
||||
}
|
||||
userNode, ok := config.AuthInfos[string(userName)]
|
||||
if !ok {
|
||||
@@ -59,20 +58,16 @@ func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextN
|
||||
if userNode.AuthProvider.Config == nil {
|
||||
return nil, xerrors.New("auth-provider.config is missing")
|
||||
}
|
||||
return &kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: userNode.LocationOfOrigin,
|
||||
UserName: userName,
|
||||
ContextName: contextName,
|
||||
OIDCConfig: makeOIDCConfig(userNode.AuthProvider.Config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func makeOIDCConfig(m map[string]string) kubeconfig.OIDCConfig {
|
||||
m := userNode.AuthProvider.Config
|
||||
var extraScopes []string
|
||||
if m["extra-scopes"] != "" {
|
||||
extraScopes = strings.Split(m["extra-scopes"], ",")
|
||||
}
|
||||
return kubeconfig.OIDCConfig{
|
||||
return &AuthProvider{
|
||||
LocationOfOrigin: userNode.LocationOfOrigin,
|
||||
UserName: userName,
|
||||
ContextName: contextName,
|
||||
IDPIssuerURL: m["idp-issuer-url"],
|
||||
ClientID: m["client-id"],
|
||||
ClientSecret: m["client-secret"],
|
||||
@@ -81,5 +76,5 @@ func makeOIDCConfig(m map[string]string) kubeconfig.OIDCConfig {
|
||||
ExtraScopes: extraScopes,
|
||||
IDToken: m["id-token"],
|
||||
RefreshToken: m["refresh-token"],
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/int128/kubelogin/models/kubeconfig"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
@@ -77,7 +76,7 @@ func unsetenv(t *testing.T, key string) {
|
||||
|
||||
func Test_findCurrentAuthProvider(t *testing.T) {
|
||||
t.Run("CurrentContext", func(t *testing.T) {
|
||||
auth, err := findCurrentAuthProvider(&api.Config{
|
||||
got, err := findCurrentAuthProvider(&api.Config{
|
||||
CurrentContext: "theContext",
|
||||
Contexts: map[string]*api.Context{
|
||||
"theContext": {
|
||||
@@ -106,28 +105,26 @@ func Test_findCurrentAuthProvider(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find the current auth: %s", err)
|
||||
}
|
||||
want := &kubeconfig.AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "theUser",
|
||||
ContextName: "theContext",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "GOOGLE_CLIENT_ID",
|
||||
ClientSecret: "GOOGLE_CLIENT_SECRET",
|
||||
IDPCertificateAuthority: "/path/to/cert",
|
||||
IDPCertificateAuthorityData: "BASE64",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
},
|
||||
want := &AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "theUser",
|
||||
ContextName: "theContext",
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
ClientID: "GOOGLE_CLIENT_ID",
|
||||
ClientSecret: "GOOGLE_CLIENT_SECRET",
|
||||
IDPCertificateAuthority: "/path/to/cert",
|
||||
IDPCertificateAuthorityData: "BASE64",
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
}
|
||||
if diff := deep.Equal(want, auth); diff != nil {
|
||||
t.Error(diff)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ByContextName", func(t *testing.T) {
|
||||
auth, err := findCurrentAuthProvider(&api.Config{
|
||||
got, err := findCurrentAuthProvider(&api.Config{
|
||||
Contexts: map[string]*api.Context{
|
||||
"theContext": {
|
||||
AuthInfo: "theUser",
|
||||
@@ -148,21 +145,19 @@ func Test_findCurrentAuthProvider(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find the current auth: %s", err)
|
||||
}
|
||||
want := &kubeconfig.AuthProvider{
|
||||
want := &AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "theUser",
|
||||
ContextName: "theContext",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
},
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
}
|
||||
if diff := deep.Equal(want, auth); diff != nil {
|
||||
t.Error(diff)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ByUserName", func(t *testing.T) {
|
||||
auth, err := findCurrentAuthProvider(&api.Config{
|
||||
got, err := findCurrentAuthProvider(&api.Config{
|
||||
AuthInfos: map[string]*api.AuthInfo{
|
||||
"theUser": {
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
@@ -178,15 +173,13 @@ func Test_findCurrentAuthProvider(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find the current auth: %s", err)
|
||||
}
|
||||
want := &kubeconfig.AuthProvider{
|
||||
want := &AuthProvider{
|
||||
LocationOfOrigin: "/path/to/kubeconfig",
|
||||
UserName: "theUser",
|
||||
OIDCConfig: kubeconfig.OIDCConfig{
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
},
|
||||
IDPIssuerURL: "https://accounts.google.com",
|
||||
}
|
||||
if diff := deep.Equal(want, auth); diff != nil {
|
||||
t.Error(diff)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
63
pkg/adaptors/kubeconfig/mock_kubeconfig/mock_kubeconfig.go
Normal file
63
pkg/adaptors/kubeconfig/mock_kubeconfig/mock_kubeconfig.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/int128/kubelogin/pkg/adaptors/kubeconfig (interfaces: Interface)
|
||||
|
||||
// Package mock_kubeconfig is a generated GoMock package.
|
||||
package mock_kubeconfig
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
kubeconfig "github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockInterface is a mock of Interface interface
|
||||
type MockInterface struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockInterfaceMockRecorder
|
||||
}
|
||||
|
||||
// MockInterfaceMockRecorder is the mock recorder for MockInterface
|
||||
type MockInterfaceMockRecorder struct {
|
||||
mock *MockInterface
|
||||
}
|
||||
|
||||
// NewMockInterface creates a new mock instance
|
||||
func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
|
||||
mock := &MockInterface{ctrl: ctrl}
|
||||
mock.recorder = &MockInterfaceMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetCurrentAuthProvider mocks base method
|
||||
func (m *MockInterface) GetCurrentAuthProvider(arg0 string, arg1 kubeconfig.ContextName, arg2 kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCurrentAuthProvider", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*kubeconfig.AuthProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCurrentAuthProvider indicates an expected call of GetCurrentAuthProvider
|
||||
func (mr *MockInterfaceMockRecorder) GetCurrentAuthProvider(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentAuthProvider", reflect.TypeOf((*MockInterface)(nil).GetCurrentAuthProvider), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// UpdateAuthProvider mocks base method
|
||||
func (m *MockInterface) UpdateAuthProvider(arg0 *kubeconfig.AuthProvider) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAuthProvider", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateAuthProvider indicates an expected call of UpdateAuthProvider
|
||||
func (mr *MockInterfaceMockRecorder) UpdateAuthProvider(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthProvider", reflect.TypeOf((*MockInterface)(nil).UpdateAuthProvider), arg0)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user