Compare commits

..

2 Commits

Author SHA1 Message Date
Hidetake Iwata
fc31d467e4 Dump all claims of ID token to debug log (#68)
* Dump all claims of ID token to debug log

* Add dump when a user already has a token
2019-04-19 10:47:32 +09:00
Hidetake Iwata
6ae8e36683 Fix error of go get and remove golint (#69)
* Remove golint

* Fix error of go get github.com/int128/ghcp
2019-04-19 10:46:07 +09:00
161 changed files with 3071 additions and 9056 deletions

View File

@@ -1,17 +0,0 @@
.PHONY: all
all:
.PHONY: install-test-deps
install-test-deps:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(HOME)/go/bin v1.24.0
go get -v github.com/int128/goxzst
.PHONY: install-release-deps
install-release-deps: go
go get -v github.com/int128/goxzst github.com/int128/ghcp
go:
curl -sSfL -o go.tgz "https://golang.org/dl/go`ruby go_version_from_config.rb < config.yml`.darwin-amd64.tar.gz"
tar -xf go.tgz
rm go.tgz
./go/bin/go version

View File

@@ -1,60 +1,38 @@
version: 2.1
version: 2
jobs:
test:
build:
docker:
- image: cimg/go:1.14.6
- image: circleci/golang:1.12.3
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: |
go get -v \
github.com/int128/goxzst \
github.com/tcnksm/ghr
- checkout
- restore_cache:
keys:
- go-sum-{{ checksum "go.sum" }}
- run: make -C .circleci install-test-deps
# workaround for https://github.com/golang/go/issues/27925
- run: sed -e '/^k8s.io\/client-go /d' -i go.sum
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- run: make dist
- save_cache:
key: go-sum-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
- store_artifacts:
path: gotest.log
release:
macos:
# https://circleci.com/docs/2.0/testing-ios/
xcode: 11.5.0
steps:
- run: echo 'export PATH="$HOME/go/bin:$PWD/.circleci/go/bin:$PATH"' >> $BASH_ENV
- checkout
- restore_cache:
keys:
- go-macos-{{ checksum "go.sum" }}
- run: make -C .circleci install-release-deps
- run: make dist
- run: make run
- run: |
if [ "$CIRCLE_TAG" ]; then
make release
fi
- save_cache:
key: go-macos-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
workflows:
version: 2
build:
all:
jobs:
- test:
filters:
tags:
only: /^v.*/
- release:
- build:
context: open-source
requires:
- test
filters:
branches:
only: /^release-feature.*/
tags:
only: /^v.*/
only: /.*/

View File

@@ -1,3 +0,0 @@
module github.com/int128/kubelogin/.circleci
go 1.13

View File

@@ -1,11 +0,0 @@
require 'yaml'
config = YAML.load(STDIN)
image = config["jobs"]["test"]["docker"][0]["image"]
if !image.start_with?("cimg/go:")
raise "unknown image #{image} in #{configPath}"
end
goVersion = image.delete_prefix("cimg/go:")
print(goVersion)

12
.github/FUNDING.yml vendored
View File

@@ -1,12 +0,0 @@
# 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']

View File

@@ -1,23 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment**
- OS: [e.g. macOS, Linux]
- kubelogin version: [e.g. 1.19.3]
- kubectl version: [e.g. 1.19]
- OpenID Connect provider: [e.g. Google, Okta]

View File

@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

View File

@@ -1,8 +0,0 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
]
}

View File

@@ -1,29 +0,0 @@
on: [push]
jobs:
system-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.8.1/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 update
- run: sudo apt install -y libnss3-tools
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
- run: make -C system_test -j3 setup
- run: make -C system_test test

12
.gitignore vendored
View File

@@ -1,13 +1,5 @@
/.idea
/system_test/output/
/acceptance_test/output/
/dist/output
/coverage.out
/gotest.log
/dist
/kubelogin
/kubectl-oidc_login
/.circleci/go/
/.kubeconfig

View File

@@ -1,49 +1,35 @@
# CircleCI specific variables
CIRCLE_TAG ?= latest
GITHUB_USERNAME := $(CIRCLE_PROJECT_USERNAME)
GITHUB_REPONAME := $(CIRCLE_PROJECT_REPONAME)
TARGET := kubelogin
TARGET_OSARCH := linux_amd64 darwin_amd64 windows_amd64 linux_arm linux_arm64
VERSION ?= $(CIRCLE_TAG)
LDFLAGS := -X main.version=$(VERSION)
TARGET_PLUGIN := kubectl-oidc_login
CIRCLE_TAG ?= HEAD
LDFLAGS := -X main.version=$(CIRCLE_TAG)
.PHONY: check run release clean
all: $(TARGET)
$(TARGET): $(wildcard **/*.go)
check:
go vet
$(MAKE) -C adaptors_test/keys/testdata
go test -v -race ./...
$(TARGET): $(wildcard *.go)
go build -o $@ -ldflags "$(LDFLAGS)"
.PHONY: check
check:
golangci-lint run
go test -v -race -cover -coverprofile=coverage.out ./... > gotest.log
$(TARGET_PLUGIN): $(TARGET)
ln -sf $(TARGET) $@
.PHONY: dist
dist: dist/output
dist/output:
# make the zip files for GitHub Releases
VERSION=$(VERSION) 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
run: $(TARGET_PLUGIN)
-PATH=.:$(PATH) kubectl oidc-login --help
dist:
VERSION=$(CIRCLE_TAG) goxzst -d dist/gh/ -o "$(TARGET)" -t "kubelogin.rb oidc-login.yaml" -- -ldflags "$(LDFLAGS)"
mv dist/gh/kubelogin.rb dist/
.PHONY: release
release: dist
# 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
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
.PHONY: clean
clean:
-rm $(TARGET)
-rm -r dist/output/
-rm coverage.out gotest.log
-rm $(TARGET_PLUGIN)
-rm -r dist/

396
README.md
View File

@@ -1,338 +1,140 @@
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/int128/kubelogin) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
# kubelogin [![CircleCI](https://circleci.com/gh/int128/kubelogin.svg?style=shield)](https://circleci.com/gh/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`.
Here is an example of Kubernetes authentication with the Google Identity Platform:
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/85427290-86e43700-b5b6-11ea-9e97-ffefd736c9b7.gif" width="572" height="391">
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:
![Diagram of the credential plugin](docs/credential-plugin-diagram.svg)
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
It gets a token from the OIDC provider and writes it to the kubeconfig.
## Getting Started
### Setup
You need to setup the following components:
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).
- OIDC provider
- Kubernetes API server
- Role for your group or user
- kubectl authentication
You can install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew) or [GitHub Releases](https://github.com/int128/kubelogin/releases) as follows:
```sh
# Homebrew (macOS and Linux)
brew install int128/kubelogin/kubelogin
# Homebrew
brew tap int128/kubelogin
brew install kubelogin
# Krew (macOS, Linux, Windows and ARM)
# Krew
kubectl krew install oidc-login
# GitHub Releases
curl -LO https://github.com/int128/kubelogin/releases/download/v1.9.1/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip
ln -s kubelogin kubectl-oidc_login
```
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
The kubeconfig looks like:
After initial setup or when the token has been expired, just run:
```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
```
% kubelogin
Using current-context: hello.k8s.local
Open http://localhost:8000 for authorization
Got a token for subject 0123456789 (valid until 2019-04-12 11:00:49 +0900 JST)
Updated ~/.kube/config
```
See [setup guide](docs/setup.md) for more.
or run as a kubectl plugin:
```
% kubectl oidc-login
```
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
For more, see the following documents:
- [Getting Started with Keycloak](docs/keycloak.md)
- [Getting Started with Google Identity Platform](docs/google.md)
- [Team Operation](docs/team_ops.md)
### Run
## Configuration
Run kubectl.
This supports the following options.
```
kubelogin [OPTIONS]
Application Options:
--kubeconfig= Path to the kubeconfig file (default: ~/.kube/config) [$KUBECONFIG]
--listen-port= Port used by kubelogin to bind its webserver (default: 8000) [$KUBELOGIN_LISTEN_PORT]
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
[$KUBELOGIN_INSECURE_SKIP_TLS_VERIFY]
--skip-open-browser If set, it does not open the browser on authentication. [$KUBELOGIN_SKIP_OPEN_BROWSER]
-v, --v= If set to 1 or greater, show debug log (default: 0)
Help Options:
-h, --help Show this help message
```
This also supports the following keys of `auth-provider` in kubeconfig.
See [kubectl authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-kubectl).
Key | Direction | Value
----|-----------|------
`idp-issuer-url` | Read (Mandatory) | Issuer URL of the provider.
`client-id` | Read (Mandatory) | Client ID of the provider.
`client-secret` | Read (Mandatory) | Client Secret of the provider.
`idp-certificate-authority` | Read | CA certificate path of the provider.
`idp-certificate-authority-data` | Read | Base64 encoded CA certificate of the provider.
`extra-scopes` | Read | Scopes to request to the provider (comma separated).
`id-token` | Write | ID token got from the provider.
`refresh-token` | Write | Refresh token got from the provider.
### Kubeconfig path
You can set the environment variable `KUBECONFIG` to point the config file.
Default to `~/.kube/config`.
```sh
kubectl get pods
export KUBECONFIG="$PWD/.kubeconfig"
```
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, 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
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
```
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 claims of an ID token by `setup` command.
```console
% kubectl oidc-login setup --oidc-issuer-url https://accounts.google.com --oidc-client-id REDACTED --oidc-client-secret REDACTED
authentication in progress...
## 2. Verify authentication
You got a token with the following claims:
{
"sub": "********",
"iss": "https://accounts.google.com",
"aud": "********",
...
}
```
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
## 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).
Kubelogin supports the following options:
```
Usage:
kubelogin get-token [flags]
Flags:
--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
--token-cache-dir string Path to a directory for token cache (default "~/.kube/cache/oidc-login")
--certificate-authority string Path to a cert file for the certificate authority
--certificate-authority-data string Base64 encoded cert for the certificate authority
--insecure-skip-tls-verify If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--grant-type string Authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-address strings [authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--skip-open-browser [authcode] Do not open the browser automatically
--open-url-after-authentication string [authcode] If set, open the URL in the browser after authentication
--oidc-redirect-url-hostname string [authcode] Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString [authcode, authcode-keyboard] Extra query parameters to send with an authentication request (default [])
--username string [password] Username for resource owner password credentials grant
--password string [password] Password for resource owner password credentials grant
-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
```
See also the options of [standalone mode](docs/standalone-mode.md).
### Extra scopes
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
You can set extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```yaml
- --oidc-extra-scope=email
- --oidc-extra-scope=profile
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
### CA Certificate
Note that kubectl does not accept multiple scopes and you need to edit the kubeconfig as like:
You can use your self-signed certificate for the provider.
```yaml
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
- --certificate-authority-data=LS0t...
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
### CA Certificates
You can set your self-signed certificates for the OIDC provider (not Kubernetes API server) by `idp-certificate-authority` and `idp-certificate-authority-data` in the kubeconfig.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```
If kubelogin could not parse the certificate, it shows a warning and skips it.
### 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
#### Authorization code flow
Kubelogin performs the authorization code flow by default.
It starts the local server at port 8000 or 18000 by default.
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 listening address.
```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
```
When authentication completed, kubelogin shows a message to close the browser.
You can change the URL to show after authentication.
```yaml
- --open-url-after-authentication=https://example.com/success.html
```
#### 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
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.
You can set the username and password.
```yaml
- --username=USERNAME
- --password=PASSWORD
```
If the password is not set, kubelogin will show the prompt for the password.
```yaml
- --username=USERNAME
```
```
% kubectl get pods
Password:
```
If the username is not set, kubelogin will show the prompt for the username and password.
```yaml
- --grant-type=password
```
```
% kubectl get pods
Username: foo
Password:
```
### Docker
You can run [the Docker image](https://quay.io/repository/int128/kubelogin) instead of the binary.
The kubeconfig looks like:
```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
- 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:
- It cannot open the browser automatically.
- The container port and listen port must be equal for consistency of the redirect URI.
## Related works
### Kubernetes Dashboard
You can access the Kubernetes Dashboard using kubelogin and [kauthproxy](https://github.com/int128/kauthproxy).
## Contributions
This is an open source software licensed under Apache License 2.0.
Feel free to open issues and pull requests for improving code and documents.
### Development
Go 1.13 or later is required.
```sh
# Run lint and tests
make check
# Compile and run the command
make
./kubelogin
```
See also [the system test](system_test).

View File

@@ -1,42 +0,0 @@
CLUSTER_NAME := kubelogin-acceptance-test
OUTPUT_DIR := $(CURDIR)/output
KUBECONFIG := $(OUTPUT_DIR)/kubeconfig.yaml
export KUBECONFIG
# create a Kubernetes cluster
.PHONY: cluster
cluster:
# create a cluster
mkdir -p $(OUTPUT_DIR)
sed -e "s|OIDC_ISSUER_URL|$(OIDC_ISSUER_URL)|" -e "s|OIDC_CLIENT_ID|$(OIDC_CLIENT_ID)|" cluster.yaml > $(OUTPUT_DIR)/cluster.yaml
kind create cluster --name $(CLUSTER_NAME) --config $(OUTPUT_DIR)/cluster.yaml
# set up access control
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=$(YOUR_EMAIL)
# set up kubectl
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=$(CURDIR)/../kubelogin \
--exec-arg=get-token \
--exec-arg=--token-cache-dir=$(OUTPUT_DIR)/token-cache \
--exec-arg=--oidc-issuer-url=$(OIDC_ISSUER_URL) \
--exec-arg=--oidc-client-id=$(OIDC_CLIENT_ID) \
--exec-arg=--oidc-client-secret=$(OIDC_CLIENT_SECRET) \
--exec-arg=--oidc-extra-scope=email
# switch the default user
kubectl config set-context --current --user=oidc
# clean up the resources
.PHONY: clean
clean:
-rm -r $(OUTPUT_DIR)
.PHONY: delete-cluster
delete-cluster:
kind delete cluster --name $(CLUSTER_NAME)
.PHONY: check
check:
docker version
kind version
kubectl version --client

View File

@@ -1,75 +0,0 @@
# kubelogin/acceptance_test
This is a manual test for verifying Kubernetes OIDC authentication with your OIDC provider.
## Purpose
This test checks the following points:
1. You can set up your OIDC provider using [setup guide](../docs/setup.md).
1. The plugin works with your OIDC provider.
## Getting Started
### Prerequisite
You need to build the plugin into the parent directory.
```sh
make -C ..
```
You need to set up your provider.
See [setup guide](../docs/setup.md) for more.
You need to install the following tools:
- Docker
- Kind
- kubectl
You can check if the tools are available.
```sh
make check
```
### 1. Create a cluster
Create a cluster.
For example, you can create a cluster with Google account authentication.
```sh
make OIDC_ISSUER_URL=https://accounts.google.com \
OIDC_CLIENT_ID=REDACTED.apps.googleusercontent.com \
OIDC_CLIENT_SECRET=REDACTED \
YOUR_EMAIL=REDACTED@gmail.com
```
It will do the following steps:
1. Create a cluster.
1. Set up access control. It allows read-only access from your email address.
1. Set up kubectl to enable the plugin.
You can change kubectl configuration in generated `output/kubeconfig.yaml`.
### 2. Run kubectl
Make sure you can log in to the provider and access the cluster.
```console
% export KUBECONFIG=$PWD/output/kubeconfig.yaml
% kubectl get pods -A
```
### Clean up
To delete the cluster and generated files:
```sh
make delete-cluster
make clean
```

View File

@@ -1,13 +0,0 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
metadata:
name: config
apiServer:
extraArgs:
oidc-issuer-url: OIDC_ISSUER_URL
oidc-client-id: OIDC_CLIENT_ID
oidc-username-claim: email

75
adaptors/cmd.go Normal file
View File

@@ -0,0 +1,75 @@
package adaptors
import (
"context"
"fmt"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/jessevdk/go-flags"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"go.uber.org/dig"
)
func NewCmd(i Cmd) adaptors.Cmd {
return &i
}
type Cmd struct {
dig.In
Login usecases.Login
Logger adaptors.Logger
}
func (cmd *Cmd) Run(ctx context.Context, args []string, version string) int {
var o cmdOptions
parser := flags.NewParser(&o, flags.HelpFlag)
parser.LongDescription = fmt.Sprintf(`Version %s
This updates the kubeconfig for Kubernetes OpenID Connect (OIDC) authentication.`,
version)
args, err := parser.ParseArgs(args[1:])
if err != nil {
cmd.Logger.Printf("Error: %s", err)
return 1
}
if len(args) > 0 {
cmd.Logger.Printf("Error: too many arguments")
return 1
}
cmd.Logger.SetLevel(adaptors.LogLevel(o.Verbose))
kubeConfig, err := o.ExpandKubeConfig()
if err != nil {
cmd.Logger.Printf("Error: invalid option: %s", err)
return 1
}
in := usecases.LoginIn{
KubeConfig: kubeConfig,
ListenPort: o.ListenPort,
SkipTLSVerify: o.SkipTLSVerify,
SkipOpenBrowser: o.SkipOpenBrowser,
}
if err := cmd.Login.Do(ctx, in); err != nil {
cmd.Logger.Printf("Error: %s", err)
return 1
}
return 0
}
type cmdOptions struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
ListenPort int `long:"listen-port" default:"8000" env:"KUBELOGIN_LISTEN_PORT" description:"Port used by kubelogin to bind its webserver"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
SkipOpenBrowser bool `long:"skip-open-browser" env:"KUBELOGIN_SKIP_OPEN_BROWSER" description:"If set, it does not open the browser on authentication."`
Verbose int `long:"v" short:"v" default:"0" description:"If set to 1 or greater, show debug log"`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *cmdOptions) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", errors.Wrapf(err, "could not expand %s", c.KubeConfig)
}
return d, nil
}

98
adaptors/cmd_test.go Normal file
View File

@@ -0,0 +1,98 @@
package adaptors
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
"github.com/int128/kubelogin/usecases/interfaces"
"github.com/int128/kubelogin/usecases/mock_usecases"
"github.com/mitchellh/go-homedir"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("Defaults", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 8000,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
SetLevel(adaptors.LogLevel(0))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("FullOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
login := mock_usecases.NewMockLogin(ctrl)
login.EXPECT().
Do(ctx, usecases.LoginIn{
KubeConfig: expand(t, "~/.kube/config"),
ListenPort: 10080,
SkipTLSVerify: true,
SkipOpenBrowser: true,
})
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
SetLevel(adaptors.LogLevel(1))
cmd := Cmd{
Login: login,
Logger: logger,
}
exitCode := cmd.Run(ctx, []string{executable,
"--listen-port", "10080",
"--insecure-skip-tls-verify",
"--skip-open-browser",
"-v1",
}, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Login: mock_usecases.NewMockLogin(ctrl),
Logger: mock_adaptors.NewLogger(t, ctrl),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
}
func expand(t *testing.T, path string) string {
d, err := homedir.Expand(path)
if err != nil {
t.Fatalf("could not expand: %s", err)
}
return d
}

82
adaptors/http.go Normal file
View File

@@ -0,0 +1,82 @@
package adaptors
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"net/http"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/infrastructure"
"github.com/pkg/errors"
"go.uber.org/dig"
)
func NewHTTP(i HTTP) adaptors.HTTP {
return &i
}
type HTTP struct {
dig.In
Logger adaptors.Logger
}
func (*HTTP) NewClientConfig() adaptors.HTTPClientConfig {
return &httpClientConfig{
certPool: x509.NewCertPool(),
}
}
func (h *HTTP) NewClient(config adaptors.HTTPClientConfig) (*http.Client, error) {
return &http.Client{
Transport: &infrastructure.LoggingTransport{
Base: &http.Transport{
TLSClientConfig: config.TLSConfig(),
Proxy: http.ProxyFromEnvironment,
},
Logger: h.Logger,
},
}, nil
}
type httpClientConfig struct {
certPool *x509.CertPool
skipTLSVerify bool
}
func (c *httpClientConfig) AddCertificateFromFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return errors.Wrapf(err, "could not read %s", filename)
}
if c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate from %s", filename)
}
return nil
}
func (c *httpClientConfig) AddEncodedCertificate(base64String string) error {
b, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return errors.Wrapf(err, "could not decode base64")
}
if c.certPool.AppendCertsFromPEM(b) != true {
return errors.Errorf("could not append certificate")
}
return nil
}
func (c *httpClientConfig) TLSConfig() *tls.Config {
tlsConfig := &tls.Config{
InsecureSkipVerify: c.skipTLSVerify,
}
if len(c.certPool.Subjects()) > 0 {
tlsConfig.RootCAs = c.certPool
}
return tlsConfig
}
func (c *httpClientConfig) SetSkipTLSVerify(b bool) {
c.skipTLSVerify = b
}

View File

@@ -0,0 +1,82 @@
package adaptors
import (
"context"
"crypto/tls"
"net/http"
"github.com/coreos/go-oidc"
"k8s.io/client-go/tools/clientcmd/api"
)
//go:generate mockgen -package mock_adaptors -destination ../mock_adaptors/mock_adaptors.go github.com/int128/kubelogin/adaptors/interfaces KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger
type Cmd interface {
Run(ctx context.Context, args []string, version string) int
}
type KubeConfig interface {
LoadFromFile(filename string) (*api.Config, error)
WriteToFile(config *api.Config, filename string) error
}
type HTTP interface {
NewClientConfig() HTTPClientConfig
NewClient(config HTTPClientConfig) (*http.Client, error)
}
type HTTPClientConfig interface {
AddCertificateFromFile(filename string) error
AddEncodedCertificate(base64String string) error
SetSkipTLSVerify(b bool)
TLSConfig() *tls.Config
}
type OIDC interface {
Authenticate(ctx context.Context, in OIDCAuthenticateIn, cb OIDCAuthenticateCallback) (*OIDCAuthenticateOut, error)
VerifyIDToken(ctx context.Context, in OIDCVerifyTokenIn) (*oidc.IDToken, error)
}
type OIDCAuthenticateIn struct {
Issuer string
ClientID string
ClientSecret string
ExtraScopes []string // Additional scopes
Client *http.Client // HTTP client for oidc and oauth2
LocalServerPort int // HTTP server port
SkipOpenBrowser bool // skip opening browser if true
}
type OIDCAuthenticateCallback struct {
ShowLocalServerURL func(url string)
}
type OIDCAuthenticateOut struct {
VerifiedIDToken *oidc.IDToken
IDToken string
RefreshToken string
}
type OIDCVerifyTokenIn struct {
IDToken string
Issuer string
ClientID string
Client *http.Client
}
type Logger interface {
Printf(format string, v ...interface{})
Debugf(level LogLevel, format string, v ...interface{})
SetLevel(level LogLevel)
IsEnabled(level LogLevel) bool
}
// LogLevel represents a log level for debug.
//
// 0 = None
// 1 = Including in/out
// 2 = Including transport headers
// 3 = Including transport body
//
type LogLevel int

30
adaptors/kubeconfig.go Normal file
View File

@@ -0,0 +1,30 @@
package adaptors
import (
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
func NewKubeConfig() adaptors.KubeConfig {
return &KubeConfig{}
}
type KubeConfig struct{}
func (*KubeConfig) LoadFromFile(filename string) (*api.Config, error) {
config, err := clientcmd.LoadFromFile(filename)
if err != nil {
return nil, errors.Wrapf(err, "could not read the kubeconfig from %s", filename)
}
return config, err
}
func (*KubeConfig) WriteToFile(config *api.Config, filename string) error {
err := clientcmd.WriteToFile(*config, filename)
if err != nil {
return errors.Wrapf(err, "could not write the kubeconfig to %s", filename)
}
return err
}

49
adaptors/logger.go Normal file
View File

@@ -0,0 +1,49 @@
package adaptors
import (
"log"
"os"
"github.com/int128/kubelogin/adaptors/interfaces"
)
// NewLogger returns a Logger with the standard log.Logger for messages and debug.
func NewLogger() adaptors.Logger {
return &Logger{
stdLogger: log.New(os.Stderr, "", 0),
debugLogger: log.New(os.Stderr, "", log.Ltime|log.Lmicroseconds),
}
}
// NewLoggerWith returns a Logger with the given standard log.Logger.
func NewLoggerWith(l stdLogger) *Logger {
return &Logger{
stdLogger: l,
debugLogger: l,
}
}
type stdLogger interface {
Printf(format string, v ...interface{})
}
// Logger wraps the standard log.Logger and just provides debug level.
type Logger struct {
stdLogger
debugLogger stdLogger
level adaptors.LogLevel
}
func (l *Logger) Debugf(level adaptors.LogLevel, format string, v ...interface{}) {
if l.IsEnabled(level) {
l.debugLogger.Printf(format, v...)
}
}
func (l *Logger) SetLevel(level adaptors.LogLevel) {
l.level = level
}
func (l *Logger) IsEnabled(level adaptors.LogLevel) bool {
return level <= l.level
}

53
adaptors/logger_test.go Normal file
View File

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

View File

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

View File

@@ -0,0 +1,305 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/adaptors/interfaces (interfaces: KubeConfig,HTTP,HTTPClientConfig,OIDC,Logger)
// Package mock_adaptors is a generated GoMock package.
package mock_adaptors
import (
context "context"
tls "crypto/tls"
go_oidc "github.com/coreos/go-oidc"
gomock "github.com/golang/mock/gomock"
interfaces "github.com/int128/kubelogin/adaptors/interfaces"
api "k8s.io/client-go/tools/clientcmd/api"
http "net/http"
reflect "reflect"
)
// MockKubeConfig is a mock of KubeConfig interface
type MockKubeConfig struct {
ctrl *gomock.Controller
recorder *MockKubeConfigMockRecorder
}
// MockKubeConfigMockRecorder is the mock recorder for MockKubeConfig
type MockKubeConfigMockRecorder struct {
mock *MockKubeConfig
}
// NewMockKubeConfig creates a new mock instance
func NewMockKubeConfig(ctrl *gomock.Controller) *MockKubeConfig {
mock := &MockKubeConfig{ctrl: ctrl}
mock.recorder = &MockKubeConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockKubeConfig) EXPECT() *MockKubeConfigMockRecorder {
return m.recorder
}
// LoadFromFile mocks base method
func (m *MockKubeConfig) LoadFromFile(arg0 string) (*api.Config, error) {
ret := m.ctrl.Call(m, "LoadFromFile", arg0)
ret0, _ := ret[0].(*api.Config)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadFromFile indicates an expected call of LoadFromFile
func (mr *MockKubeConfigMockRecorder) LoadFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadFromFile", reflect.TypeOf((*MockKubeConfig)(nil).LoadFromFile), arg0)
}
// WriteToFile mocks base method
func (m *MockKubeConfig) WriteToFile(arg0 *api.Config, arg1 string) error {
ret := m.ctrl.Call(m, "WriteToFile", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// WriteToFile indicates an expected call of WriteToFile
func (mr *MockKubeConfigMockRecorder) WriteToFile(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteToFile", reflect.TypeOf((*MockKubeConfig)(nil).WriteToFile), arg0, arg1)
}
// MockHTTP is a mock of HTTP interface
type MockHTTP struct {
ctrl *gomock.Controller
recorder *MockHTTPMockRecorder
}
// MockHTTPMockRecorder is the mock recorder for MockHTTP
type MockHTTPMockRecorder struct {
mock *MockHTTP
}
// NewMockHTTP creates a new mock instance
func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP {
mock := &MockHTTP{ctrl: ctrl}
mock.recorder = &MockHTTPMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder {
return m.recorder
}
// NewClient mocks base method
func (m *MockHTTP) NewClient(arg0 interfaces.HTTPClientConfig) (*http.Client, error) {
ret := m.ctrl.Call(m, "NewClient", arg0)
ret0, _ := ret[0].(*http.Client)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewClient indicates an expected call of NewClient
func (mr *MockHTTPMockRecorder) NewClient(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClient", reflect.TypeOf((*MockHTTP)(nil).NewClient), arg0)
}
// NewClientConfig mocks base method
func (m *MockHTTP) NewClientConfig() interfaces.HTTPClientConfig {
ret := m.ctrl.Call(m, "NewClientConfig")
ret0, _ := ret[0].(interfaces.HTTPClientConfig)
return ret0
}
// NewClientConfig indicates an expected call of NewClientConfig
func (mr *MockHTTPMockRecorder) NewClientConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewClientConfig", reflect.TypeOf((*MockHTTP)(nil).NewClientConfig))
}
// MockHTTPClientConfig is a mock of HTTPClientConfig interface
type MockHTTPClientConfig struct {
ctrl *gomock.Controller
recorder *MockHTTPClientConfigMockRecorder
}
// MockHTTPClientConfigMockRecorder is the mock recorder for MockHTTPClientConfig
type MockHTTPClientConfigMockRecorder struct {
mock *MockHTTPClientConfig
}
// NewMockHTTPClientConfig creates a new mock instance
func NewMockHTTPClientConfig(ctrl *gomock.Controller) *MockHTTPClientConfig {
mock := &MockHTTPClientConfig{ctrl: ctrl}
mock.recorder = &MockHTTPClientConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockHTTPClientConfig) EXPECT() *MockHTTPClientConfigMockRecorder {
return m.recorder
}
// AddCertificateFromFile mocks base method
func (m *MockHTTPClientConfig) AddCertificateFromFile(arg0 string) error {
ret := m.ctrl.Call(m, "AddCertificateFromFile", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddCertificateFromFile indicates an expected call of AddCertificateFromFile
func (mr *MockHTTPClientConfigMockRecorder) AddCertificateFromFile(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCertificateFromFile", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddCertificateFromFile), arg0)
}
// AddEncodedCertificate mocks base method
func (m *MockHTTPClientConfig) AddEncodedCertificate(arg0 string) error {
ret := m.ctrl.Call(m, "AddEncodedCertificate", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AddEncodedCertificate indicates an expected call of AddEncodedCertificate
func (mr *MockHTTPClientConfigMockRecorder) AddEncodedCertificate(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddEncodedCertificate", reflect.TypeOf((*MockHTTPClientConfig)(nil).AddEncodedCertificate), arg0)
}
// SetSkipTLSVerify mocks base method
func (m *MockHTTPClientConfig) SetSkipTLSVerify(arg0 bool) {
m.ctrl.Call(m, "SetSkipTLSVerify", arg0)
}
// SetSkipTLSVerify indicates an expected call of SetSkipTLSVerify
func (mr *MockHTTPClientConfigMockRecorder) SetSkipTLSVerify(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSkipTLSVerify", reflect.TypeOf((*MockHTTPClientConfig)(nil).SetSkipTLSVerify), arg0)
}
// TLSConfig mocks base method
func (m *MockHTTPClientConfig) TLSConfig() *tls.Config {
ret := m.ctrl.Call(m, "TLSConfig")
ret0, _ := ret[0].(*tls.Config)
return ret0
}
// TLSConfig indicates an expected call of TLSConfig
func (mr *MockHTTPClientConfigMockRecorder) TLSConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TLSConfig", reflect.TypeOf((*MockHTTPClientConfig)(nil).TLSConfig))
}
// MockOIDC is a mock of OIDC interface
type MockOIDC struct {
ctrl *gomock.Controller
recorder *MockOIDCMockRecorder
}
// MockOIDCMockRecorder is the mock recorder for MockOIDC
type MockOIDCMockRecorder struct {
mock *MockOIDC
}
// NewMockOIDC creates a new mock instance
func NewMockOIDC(ctrl *gomock.Controller) *MockOIDC {
mock := &MockOIDC{ctrl: ctrl}
mock.recorder = &MockOIDCMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockOIDC) EXPECT() *MockOIDCMockRecorder {
return m.recorder
}
// Authenticate mocks base method
func (m *MockOIDC) Authenticate(arg0 context.Context, arg1 interfaces.OIDCAuthenticateIn, arg2 interfaces.OIDCAuthenticateCallback) (*interfaces.OIDCAuthenticateOut, error) {
ret := m.ctrl.Call(m, "Authenticate", arg0, arg1, arg2)
ret0, _ := ret[0].(*interfaces.OIDCAuthenticateOut)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Authenticate indicates an expected call of Authenticate
func (mr *MockOIDCMockRecorder) Authenticate(arg0, arg1, arg2 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockOIDC)(nil).Authenticate), arg0, arg1, arg2)
}
// VerifyIDToken mocks base method
func (m *MockOIDC) VerifyIDToken(arg0 context.Context, arg1 interfaces.OIDCVerifyTokenIn) (*go_oidc.IDToken, error) {
ret := m.ctrl.Call(m, "VerifyIDToken", arg0, arg1)
ret0, _ := ret[0].(*go_oidc.IDToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VerifyIDToken indicates an expected call of VerifyIDToken
func (mr *MockOIDCMockRecorder) VerifyIDToken(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyIDToken", reflect.TypeOf((*MockOIDC)(nil).VerifyIDToken), arg0, arg1)
}
// MockLogger is a mock of Logger interface
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debugf mocks base method
func (m *MockLogger) Debugf(arg0 interfaces.LogLevel, arg1 string, arg2 ...interface{}) {
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf
func (mr *MockLoggerMockRecorder) Debugf(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// IsEnabled mocks base method
func (m *MockLogger) IsEnabled(arg0 interfaces.LogLevel) bool {
ret := m.ctrl.Call(m, "IsEnabled", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// IsEnabled indicates an expected call of IsEnabled
func (mr *MockLoggerMockRecorder) IsEnabled(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsEnabled", reflect.TypeOf((*MockLogger)(nil).IsEnabled), arg0)
}
// Printf mocks base method
func (m *MockLogger) Printf(arg0 string, arg1 ...interface{}) {
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Printf", varargs...)
}
// Printf indicates an expected call of Printf
func (mr *MockLoggerMockRecorder) Printf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Printf", reflect.TypeOf((*MockLogger)(nil).Printf), varargs...)
}
// SetLevel mocks base method
func (m *MockLogger) SetLevel(arg0 interfaces.LogLevel) {
m.ctrl.Call(m, "SetLevel", arg0)
}
// SetLevel indicates an expected call of SetLevel
func (mr *MockLoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*MockLogger)(nil).SetLevel), arg0)
}

73
adaptors/oidc.go Normal file
View File

@@ -0,0 +1,73 @@
package adaptors
import (
"context"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/oauth2cli"
"github.com/pkg/errors"
"golang.org/x/oauth2"
)
func NewOIDC() adaptors.OIDC {
return &OIDC{}
}
type OIDC struct{}
func (*OIDC) Authenticate(ctx context.Context, in adaptors.OIDCAuthenticateIn, cb adaptors.OIDCAuthenticateCallback) (*adaptors.OIDCAuthenticateOut, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
flow := oauth2cli.AuthCodeFlow{
Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: in.ClientID,
ClientSecret: in.ClientSecret,
Scopes: append(in.ExtraScopes, oidc.ScopeOpenID),
},
LocalServerPort: in.LocalServerPort,
SkipOpenBrowser: in.SkipOpenBrowser,
AuthCodeOptions: []oauth2.AuthCodeOption{oauth2.AccessTypeOffline},
ShowLocalServerURL: cb.ShowLocalServerURL,
}
token, err := flow.GetToken(ctx)
if err != nil {
return nil, errors.Wrapf(err, "could not get a token")
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return &adaptors.OIDCAuthenticateOut{
VerifiedIDToken: verifiedIDToken,
IDToken: idToken,
RefreshToken: token.RefreshToken,
}, nil
}
func (*OIDC) VerifyIDToken(ctx context.Context, in adaptors.OIDCVerifyTokenIn) (*oidc.IDToken, error) {
if in.Client != nil {
ctx = context.WithValue(ctx, oauth2.HTTPClient, in.Client)
}
provider, err := oidc.NewProvider(ctx, in.Issuer)
if err != nil {
return nil, errors.Wrapf(err, "could not discovery the OIDC issuer")
}
verifier := provider.Verifier(&oidc.Config{ClientID: in.ClientID})
verifiedIDToken, err := verifier.Verify(ctx, in.IDToken)
if err != nil {
return nil, errors.Wrapf(err, "could not verify the id_token")
}
return verifiedIDToken, nil
}

View File

@@ -0,0 +1,38 @@
package authserver
import (
"crypto/rsa"
"net/http"
"testing"
)
// Config represents server configuration.
type Config struct {
Issuer string
Scope string
TLSServerCert string
TLSServerKey string
IDToken string
RefreshToken string
IDTokenKeyPair *rsa.PrivateKey
}
// Start starts a HTTP server.
func Start(t *testing.T, c Config) *http.Server {
s := &http.Server{
Addr: "localhost:9000",
Handler: newHandler(t, c),
}
go func() {
var err error
if c.TLSServerCert != "" && c.TLSServerKey != "" {
err = s.ListenAndServeTLS(c.TLSServerCert, c.TLSServerKey)
} else {
err = s.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return s
}

View File

@@ -0,0 +1,108 @@
package authserver
import (
"encoding/base64"
"fmt"
"math/big"
"net/http"
"testing"
"text/template"
"github.com/pkg/errors"
)
type handler struct {
t *testing.T
discovery *template.Template
token *template.Template
jwks *template.Template
authCode string
// Template values
Issuer string
Scope string // Default to openid
IDToken string
RefreshToken string
PrivateKey struct{ N, E string }
}
func newHandler(t *testing.T, c Config) *handler {
tpl, err := template.ParseFiles(
"authserver/testdata/oidc-discovery.json",
"authserver/testdata/oidc-token.json",
"authserver/testdata/oidc-jwks.json",
)
if err != nil {
t.Fatalf("could not read the templates: %s", err)
}
h := handler{
t: t,
discovery: tpl.Lookup("oidc-discovery.json"),
token: tpl.Lookup("oidc-token.json"),
jwks: tpl.Lookup("oidc-jwks.json"),
authCode: "3d24a8bd-35e6-457d-999e-e04bb1dfcec7",
Issuer: c.Issuer,
Scope: c.Scope,
IDToken: c.IDToken,
RefreshToken: c.RefreshToken,
}
if h.Scope == "" {
h.Scope = "openid"
}
if c.IDTokenKeyPair != nil {
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(c.IDTokenKeyPair.E)).Bytes())
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(c.IDTokenKeyPair.N.Bytes())
}
return &h
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.serveHTTP(w, r); err != nil {
h.t.Logf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}
func (h *handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
h.t.Logf("[auth-server] %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := h.discovery.Execute(w, h); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
if h.Scope != q.Get("scope") {
return errors.Errorf("scope wants %s but %s", h.Scope, q.Get("scope"))
}
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), h.authCode)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
if err := r.ParseForm(); err != nil {
return errors.Wrapf(err, "could not parse the form")
}
if h.authCode != r.Form.Get("code") {
return errors.Errorf("code wants %s but %s", h.authCode, r.Form.Get("code"))
}
w.Header().Add("Content-Type", "application/json")
if err := h.token.Execute(w, h); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := h.jwks.Execute(w, h); err != nil {
return errors.Wrapf(err, "could not execute the template")
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}

View File

@@ -0,0 +1,85 @@
{
"issuer": "{{ .Issuer }}",
"authorization_endpoint": "{{ .Issuer }}/protocol/openid-connect/auth",
"token_endpoint": "{{ .Issuer }}/protocol/openid-connect/token",
"token_introspection_endpoint": "{{ .Issuer }}/protocol/openid-connect/token/introspect",
"userinfo_endpoint": "{{ .Issuer }}/protocol/openid-connect/userinfo",
"end_session_endpoint": "{{ .Issuer }}/protocol/openid-connect/logout",
"jwks_uri": "{{ .Issuer }}/protocol/openid-connect/certs",
"check_session_iframe": "{{ .Issuer }}/protocol/openid-connect/login-status-iframe.html",
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password",
"client_credentials"
],
"response_types_supported": [
"code",
"none",
"id_token",
"token",
"id_token token",
"code id_token",
"code token",
"code id_token token"
],
"subject_types_supported": [
"public",
"pairwise"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"userinfo_signing_alg_values_supported": [
"RS256"
],
"request_object_signing_alg_values_supported": [
"none",
"RS256"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"registration_endpoint": "{{ .Issuer }}/clients-registrations/openid-connect",
"token_endpoint_auth_methods_supported": [
"private_key_jwt",
"client_secret_basic",
"client_secret_post",
"client_secret_jwt"
],
"token_endpoint_auth_signing_alg_values_supported": [
"RS256"
],
"claims_supported": [
"sub",
"iss",
"auth_time",
"name",
"given_name",
"family_name",
"preferred_username",
"email"
],
"claim_types_supported": [
"normal"
],
"claims_parameter_supported": false,
"scopes_supported": [
"openid",
"offline_access",
"phone",
"address",
"email",
"profile"
],
"request_parameter_supported": true,
"request_uri_parameter_supported": true,
"code_challenge_methods_supported": [
"plain",
"S256"
],
"tls_client_certificate_bound_access_tokens": true
}

View File

@@ -0,0 +1,12 @@
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "xxx",
"n": "{{ .PrivateKey.N }}",
"e": "{{ .PrivateKey.E }}"
}
]
}

View File

@@ -0,0 +1,7 @@
{
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
"token_type": "Bearer",
"refresh_token": "{{ .RefreshToken }}",
"expires_in": 3600,
"id_token": "{{ .IDToken }}"
}

230
adaptors_test/cmd_test.go Normal file
View File

@@ -0,0 +1,230 @@
package adaptors_test
import (
"context"
"crypto/tls"
"net/http"
"os"
"testing"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors_test/authserver"
"github.com/int128/kubelogin/adaptors_test/keys"
"github.com/int128/kubelogin/adaptors_test/kubeconfig"
"github.com/int128/kubelogin/adaptors_test/logger"
"github.com/int128/kubelogin/di"
)
// Run the integration tests.
//
// 1. Start the auth server at port 9000.
// 2. Run the Cmd.
// 3. Open a request for port 8000.
// 4. Wait for the Cmd.
// 5. Shutdown the auth server.
//
func TestCmd_Run(t *testing.T) {
timeout := 1 * time.Second
t.Run("NoTLS", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9000")
serverConfig := authserver.Config{
Issuer: "http://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "http://localhost:9000",
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("ExtraScopes", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "http://localhost:9000")
serverConfig := authserver.Config{
Issuer: "http://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
Scope: "profile groups openid",
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "http://localhost:9000",
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, nil)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACert", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9000")
serverConfig := authserver.Config{
Issuer: "https://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
TLSServerCert: keys.TLSServerCert,
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthority: keys.TLSCACert,
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("CACertData", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
idToken := newIDToken(t, "https://localhost:9000")
serverConfig := authserver.Config{
Issuer: "https://localhost:9000",
IDToken: idToken,
IDTokenKeyPair: keys.JWSKeyPair,
RefreshToken: "REFRESH_TOKEN",
TLSServerCert: keys.TLSServerCert,
TLSServerKey: keys.TLSServerKey,
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "https://localhost:9000",
IDPCertificateAuthorityData: keys.TLSCACertAsBase64,
})
defer os.Remove(kubeConfigFilename)
startBrowserRequest(t, ctx, keys.TLSCACertAsConfig)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
RefreshToken: "REFRESH_TOKEN",
})
})
t.Run("AlreadyHaveValidToken", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
serverConfig := authserver.Config{
Issuer: "http://localhost:9000",
IDTokenKeyPair: keys.JWSKeyPair,
}
server := authserver.Start(t, serverConfig)
defer server.Shutdown(ctx)
idToken := newIDToken(t, "http://localhost:9000")
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: "http://localhost:9000",
IDToken: idToken,
})
defer os.Remove(kubeConfigFilename)
runCmd(t, ctx, "--kubeconfig", kubeConfigFilename, "--skip-open-browser")
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: idToken,
})
})
}
func newIDToken(t *testing.T, issuer string) string {
t.Helper()
var claims struct {
jwt.StandardClaims
Groups []string `json:"groups"`
}
claims.StandardClaims = jwt.StandardClaims{
Issuer: issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
Subject: "SUBJECT",
IssuedAt: time.Now().Unix(),
}
claims.Groups = []string{"admin", "users"}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
s, err := token.SignedString(keys.JWSKeyPair)
if err != nil {
t.Fatalf("Could not sign the claims: %s", err)
}
return s
}
func runCmd(t *testing.T, ctx context.Context, args ...string) {
t.Helper()
newLogger := func() adaptors.Logger {
return logger.New(t)
}
if err := di.InvokeWithExtra(func(cmd adaptors.Cmd) {
exitCode := cmd.Run(ctx, append([]string{"kubelogin", "--v=1"}, args...), "HEAD")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}, newLogger); err != nil {
t.Errorf("Invoke returned error: %+v", err)
}
}
func startBrowserRequest(t *testing.T, ctx context.Context, tlsConfig *tls.Config) {
t.Helper()
client := http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
req, err := http.NewRequest("GET", "http://localhost:8000/", nil)
if err != nil {
t.Errorf("could not create a request: %s", err)
return
}
req = req.WithContext(ctx)
go func() {
time.Sleep(50 * time.Millisecond)
resp, err := client.Do(req)
if err != nil {
t.Errorf("could not send a request: %s", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
}()
}

View File

@@ -0,0 +1,71 @@
package keys
import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io/ioutil"
"github.com/pkg/errors"
)
// TLSCACert is path to the CA certificate.
// This should be generated by Makefile before test.
const TLSCACert = "keys/testdata/ca.crt"
// TLSCACertAsBase64 is a base64 encoded string of TLSCACert.
var TLSCACertAsBase64 string
// TLSCACertAsConfig is a TLS config including TLSCACert.
var TLSCACertAsConfig = &tls.Config{RootCAs: x509.NewCertPool()}
// TLSServerCert is path to the server certificate.
// This should be generated by Makefile before test.
const TLSServerCert = "keys/testdata/server.crt"
// TLSServerKey is path to the server key.
// This should be generated by Makefile before test.
const TLSServerKey = "keys/testdata/server.key"
// JWSKey is path to the key for signing ID tokens.
const JWSKey = "keys/testdata/jws.key"
// JWSKeyPair is the key pair loaded from JWSKey.
var JWSKeyPair *rsa.PrivateKey
func init() {
var err error
JWSKeyPair, err = readPrivateKey(JWSKey)
if err != nil {
panic(err)
}
b, err := ioutil.ReadFile(TLSCACert)
if err != nil {
panic(err)
}
TLSCACertAsBase64 = base64.StdEncoding.EncodeToString(b)
if !TLSCACertAsConfig.RootCAs.AppendCertsFromPEM(b) {
panic("could not append the CA cert")
}
}
func readPrivateKey(name string) (*rsa.PrivateKey, error) {
b, err := ioutil.ReadFile(name)
if err != nil {
return nil, errors.Wrapf(err, "could not read JWSKey")
}
block, rest := pem.Decode(b)
if block == nil {
return nil, errors.New("could not decode PEM")
}
if len(rest) > 0 {
return nil, errors.New("PEM should contain single key but multiple keys")
}
k, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, errors.Wrapf(err, "could not parse the key")
}
return k, nil
}

View File

@@ -0,0 +1,4 @@
/CA
*.key
*.csr
*.crt

53
adaptors_test/keys/testdata/Makefile vendored Normal file
View File

@@ -0,0 +1,53 @@
.PHONY: clean
all: server.crt ca.crt jws.key
clean:
rm -v ca.* server.*
ca.key:
openssl genrsa -out $@ 1024
ca.csr: openssl.cnf ca.key
openssl req -config openssl.cnf \
-new \
-key ca.key \
-subj "/CN=Hello CA" \
-out $@
openssl req -noout -text -in $@
ca.crt: ca.csr ca.key
openssl x509 -req \
-signkey ca.key \
-in ca.csr \
-out $@
openssl x509 -text -in $@
server.key:
openssl genrsa -out $@ 1024
server.csr: openssl.cnf server.key
openssl req -config openssl.cnf \
-new \
-key server.key \
-subj "/CN=localhost" \
-out $@
openssl req -noout -text -in $@
server.crt: openssl.cnf server.csr ca.key ca.crt
rm -fr ./CA
mkdir -p ./CA
touch CA/index.txt
touch CA/index.txt.attr
echo 00 > CA/serial
openssl ca -config openssl.cnf \
-extensions v3_req \
-batch \
-cert ca.crt \
-keyfile ca.key \
-in server.csr \
-out $@
openssl x509 -text -in $@
jws.key:
openssl genrsa -out $@ 1024

37
adaptors_test/keys/testdata/openssl.cnf vendored Normal file
View File

@@ -0,0 +1,37 @@
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = ./CA
certs = $dir
crl_dir = $dir
database = $dir/index.txt
new_certs_dir = $dir
default_md = sha256
policy = policy_match
serial = $dir/serial
default_days = 365
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req
x509_extensions = v3_ca
[ req_distinguished_name ]
commonName = Common Name (e.g. server FQDN or YOUR name)
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = DNS:localhost
[ v3_ca ]
basicConstraints = CA:true

View File

@@ -16,7 +16,6 @@ type Values struct {
IDPCertificateAuthority string
IDPCertificateAuthorityData string
IDToken string
RefreshToken string
}
// Create creates a kubeconfig file and returns path to it.

View File

@@ -30,8 +30,5 @@ users:
#{{ end }}
#{{ if .IDToken }}
id-token: {{ .IDToken }}
#{{ end }}
#{{ if .RefreshToken }}
refresh-token: {{ .RefreshToken }}
#{{ end }}
name: oidc

View File

@@ -0,0 +1,21 @@
package logger
import (
"github.com/int128/kubelogin/adaptors"
)
func New(t testingLogger) *adaptors.Logger {
return adaptors.NewLoggerWith(&bridge{t})
}
type testingLogger interface {
Logf(format string, v ...interface{})
}
type bridge struct {
t testingLogger
}
func (b *bridge) Printf(format string, v ...interface{}) {
b.t.Logf(format, v...)
}

42
di/di.go Normal file
View File

@@ -0,0 +1,42 @@
// Package di provides dependency injection.
package di
import (
"github.com/int128/kubelogin/adaptors"
adaptorsInterfaces "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/usecases"
"github.com/pkg/errors"
"go.uber.org/dig"
)
var constructors = []interface{}{
usecases.NewLogin,
adaptors.NewCmd,
adaptors.NewKubeConfig,
adaptors.NewOIDC,
adaptors.NewHTTP,
}
var extraConstructors = []interface{}{
adaptors.NewLogger,
}
// Invoke runs the function with the default constructors.
func Invoke(f func(cmd adaptorsInterfaces.Cmd)) error {
return InvokeWithExtra(f, extraConstructors...)
}
// InvokeWithExtra runs the function with the given constructors.
func InvokeWithExtra(f func(cmd adaptorsInterfaces.Cmd), extra ...interface{}) error {
c := dig.New()
for _, constructor := range append(constructors, extra...) {
if err := c.Provide(constructor); err != nil {
return errors.Wrapf(err, "could not provide the constructor")
}
}
if err := c.Invoke(f); err != nil {
return errors.Wrapf(err, "could not invoke")
}
return nil
}

18
di/di_test.go Normal file
View File

@@ -0,0 +1,18 @@
package di_test
import (
"testing"
adaptors "github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
func TestInvoke(t *testing.T) {
if err := di.Invoke(func(cmd adaptors.Cmd) {
if cmd == nil {
t.Errorf("cmd wants non-nil but nil")
}
}); err != nil {
t.Fatalf("Invoke returned error: %+v", err)
}
}

13
dist/Dockerfile vendored
View File

@@ -1,13 +0,0 @@
FROM alpine:3.12
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"]

86
dist/oidc-login.yaml vendored
View File

@@ -1,86 +0,0 @@
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.
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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

100
docs/google.md Normal file
View File

@@ -0,0 +1,100 @@
# Getting Started with Google Identity Platform
## Prerequisite
- You have a Google account.
- You have the Cluster Admin role of the Kubernetes cluster.
- You can configure the Kubernetes API server.
- `kubectl` and `kubelogin` are installed to your computer.
## 1. Setup Google API
Open [Google APIs Console](https://console.developers.google.com/apis/credentials) and create an OAuth client with the following setting:
- Application Type: Other
## 2. Setup Kubernetes API server
Configure your Kubernetes API Server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
### kops
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://accounts.google.com
oidcClientID: YOUR_CLIENT_ID.apps.googleusercontent.com
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to you.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: oidc-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: User
name: https://accounts.google.com#1234567890
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://accounts.google.com \
--auth-provider-arg client-id=YOUR_CLIENT_ID.apps.googleusercontent.com \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
client-id: YOUR_CLIENT_ID.apps.googleusercontent.com
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

107
docs/keycloak.md Normal file
View File

@@ -0,0 +1,107 @@
# Getting Started with Keycloak
## Prerequisite
- You have administrator access to the Keycloak.
- 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 Keycloak
Open the Keycloak and create an OIDC client as follows:
- Redirect URL: `http://localhost:8000/`
- Issuer URL: `https://keycloak.example.com/auth/realms/YOUR_REALM`
- Client ID: `kubernetes`
- Groups claim: `groups`
Create a group `kubernetes:admin` and join to it.
This is used for group based access control.
## 2. Setup Kubernetes API server
Configure your Kubernetes API server accepts [OpenID Connect Tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens).
### kops
If you are using [kops](https://github.com/kubernetes/kops), run `kops edit cluster` and append the following settings:
```yaml
spec:
kubeAPIServer:
oidcIssuerURL: https://keycloak.example.com/auth/realms/YOUR_REALM
oidcClientID: kubernetes
oidcGroupsClaim: groups
```
## 3. Setup Kubernetes cluster
Here assign the `cluster-admin` role to the `kubernetes:admin` group.
```yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: keycloak-admin-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: /kubernetes:admin
```
You can create a custom role and assign it as well.
## 4. Setup kubectl
Configure `kubectl` for the OIDC authentication.
```sh
kubectl config set-credentials NAME \
--auth-provider oidc \
--auth-provider-arg idp-issuer-url=https://keycloak.example.com/auth/realms/YOUR_REALM \
--auth-provider-arg client-id=kubernetes \
--auth-provider-arg client-secret=YOUR_CLIENT_SECRET
```
## 5. Run kubelogin
Run `kubelogin`.
```
% kubelogin
2018/08/10 10:36:38 Reading .kubeconfig
2018/08/10 10:36:38 Using current context: hello.k8s.local
2018/08/10 10:36:41 Open http://localhost:8000 for authorization
2018/08/10 10:36:45 GET /
2018/08/10 10:37:07 GET /?state=...&session_state=...&code=ey...
2018/08/10 10:37:08 Updated .kubeconfig
```
Now your `~/.kube/config` should be like:
```yaml
users:
- name: hello.k8s.local
user:
auth-provider:
config:
idp-issuer-url: https://keycloak.example.com/auth/realms/YOUR_REALM
client-id: kubernetes
client-secret: YOUR_SECRET
id-token: ey... # kubelogin will update ID token here
refresh-token: ey... # kubelogin will update refresh token here
name: oidc
```
Make sure you can access to the Kubernetes cluster.
```
% kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-1-2-3-4.us-west-2.compute.internal Ready node 21d v1.9.6
ip-1-2-3-5.us-west-2.compute.internal Ready node 20d v1.9.6
```

View File

@@ -1,226 +0,0 @@
# 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`.
If you need `groups` claim for access control,
see [jetstack/okta-kubectl-auth](https://github.com/jetstack/okta-kubectl-auth/blob/master/docs/okta-setup.md) and [#250](https://github.com/int128/kubelogin/issues/250).
## 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.

View File

@@ -1,129 +0,0 @@
# 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
### 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
```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

40
docs/team_ops.md Normal file
View File

@@ -0,0 +1,40 @@
# Team Operation
## kops
Export the kubeconfig.
```sh
KUBECONFIG=.kubeconfig kops export kubecfg hello.k8s.local
```
Remove the `admin` access from the kubeconfig.
It should look as like:
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS...
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
auth-provider:
name: oidc
config:
client-id: YOUR_CLIEND_ID
client-secret: YOUR_CLIENT_SECRET
idp-issuer-url: YOUR_ISSUER
```
You can share the kubeconfig to your team members for easy onboarding.

49
go.mod
View File

@@ -1,26 +1,35 @@
module github.com/int128/kubelogin
go 1.12
require (
github.com/chromedp/chromedp v0.5.3
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-oidc v2.0.0+incompatible
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/golang/mock v1.4.4
github.com/google/go-cmp v0.5.1
github.com/google/wire v0.4.0
github.com/int128/oauth2cli v1.12.1
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/mock v1.2.0
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/int128/oauth2cli v1.2.1
github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.6 // indirect
github.com/mitchellh/go-homedir v1.1.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v1.0.0
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-20200625203802-6e8e738ad208
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.3.0
k8s.io/apimachinery v0.18.6
k8s.io/client-go v0.18.6
k8s.io/klog v1.0.0
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.3.0 // indirect
go.uber.org/dig v1.7.0
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
golang.org/x/net v0.0.0-20190328230028-74de082e2cca // indirect
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.3.0 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190222213804-5cb15d344471 // indirect
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 // indirect
k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.2.0 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
)

333
go.sum
View File

@@ -1,339 +1,88 @@
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-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/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/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/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/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 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
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 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
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/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.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/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/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/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/listener v1.1.0 h1:2Jb41DWLpkQ3I9bIdBzO8H/tNwMvyl/OBZWtCV5Pjuw=
github.com/int128/listener v1.1.0/go.mod h1:68WkmTN8PQtLzc9DucIaagAKeGVyMnyyKIkW4Xn47UA=
github.com/int128/oauth2cli v1.12.1 h1:F+6sykVdM+0rede+jAJ2RICP3GAsLLGvPjSFLlI0U9Q=
github.com/int128/oauth2cli v1.12.1/go.mod h1:0Wf2wAxKJNzbkPkUIYNhTjeLn/pqIBDOBAGfwrxGYQw=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/int128/oauth2cli v1.1.0 h1:qAT6C8GyaLaSf0aseQUTcJvZ+j2MueETzGkpoFow0kc=
github.com/int128/oauth2cli v1.1.0/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
github.com/int128/oauth2cli v1.2.1 h1:rhYQ++Kijz/sleAfzy2u2qEsQJCQSHVYjANgOM/LfLA=
github.com/int128/oauth2cli v1.2.1/go.mod h1:R1iBtRu+y4+DF4efDU0UePUYWjWfggwFI1KY1dw5E1M=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/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/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/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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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/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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/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/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 v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/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/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/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=
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=
go.uber.org/dig v1.7.0 h1:E5/L92iQTNJTjfgJF2KgU+/JpMaiuvK2DHLBj0+kSZk=
go.uber.org/dig v1.7.0/go.mod h1:z+dSd2TP9Usi48jL8M3v63iSBVkiwtVyMKxMZYYauPg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-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/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
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/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/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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 h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
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-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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
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/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/square/go-jose.v2 v2.3.0 h1:nLzhkFyl5bkblqYBoiWJUt5JkWOzmiaBtCxdJAqJd3U=
gopkg.in/square/go-jose.v2 v2.3.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/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.6 h1:osqrAXbOQjkKIWDTjrqxWQ3w0GkKb1KA1XkUGHHYpeE=
k8s.io/api v0.18.6/go.mod h1:eeyxr+cwCjMdLAmr2W3RyDI0VvTawSg/3RFFBEnmZGI=
k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag=
k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko=
k8s.io/client-go v0.18.6 h1:I+oWqJbibLSGsZj8Xs8F0aWVXJVIoUHWaaJV3kUN/Zw=
k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q=
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-20200410145947-61e04a5be9a6/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=
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=
k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE=
k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg=
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v10.0.0+incompatible h1:+xQQxwjrcIPWDMJBAS+1G2FNk1McoPnb53xkvcDiDqE=
k8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c=
k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=

50
infrastructure/http.go Normal file
View File

@@ -0,0 +1,50 @@
package infrastructure
import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/adaptors/interfaces"
)
const (
logLevelDumpHeaders = 2
logLevelDumpBody = 3
)
type LoggingTransport struct {
Base http.RoundTripper
Logger adaptors.Logger
}
func (t *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.IsDumpEnabled() {
return t.Base.RoundTrip(req)
}
reqDump, err := httputil.DumpRequestOut(req, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the request: %s", err)
return t.Base.RoundTrip(req)
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(reqDump))
resp, err := t.Base.RoundTrip(req)
if err != nil {
return resp, err
}
respDump, err := httputil.DumpResponse(resp, t.IsDumpBodyEnabled())
if err != nil {
t.Logger.Debugf(logLevelDumpHeaders, "Error: could not dump the response: %s", err)
return resp, err
}
t.Logger.Debugf(logLevelDumpHeaders, "%s", string(respDump))
return resp, err
}
func (t *LoggingTransport) IsDumpEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpHeaders)
}
func (t *LoggingTransport) IsDumpBodyEnabled() bool {
return t.Logger.IsEnabled(logLevelDumpBody)
}

View File

@@ -0,0 +1,90 @@
package infrastructure
import (
"bufio"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/adaptors/mock_adaptors"
)
type mockTransport struct {
req *http.Request
resp *http.Response
}
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
t.req = req
return t.resp, nil
}
func TestLoggingTransport_RoundTrip(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(gomock.Any()).
Return(true).
AnyTimes()
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
Host: example.com
dummy`)), req)
if err != nil {
t.Errorf("could not create a response: %s", err)
}
defer resp.Body.Close()
transport := &LoggingTransport{
Base: &mockTransport{resp: resp},
Logger: logger,
}
gotResp, err := transport.RoundTrip(req)
if err != nil {
t.Errorf("RoundTrip error: %s", err)
}
if gotResp != resp {
t.Errorf("resp wants %v but %v", resp, gotResp)
}
}
func TestLoggingTransport_IsDumpEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(adaptors.LogLevel(logLevelDumpHeaders)).
Return(true)
transport := &LoggingTransport{
Logger: logger,
}
if transport.IsDumpEnabled() != true {
t.Errorf("IsDumpEnabled wants true")
}
}
func TestLoggingTransport_IsDumpBodyEnabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
logger := mock_adaptors.NewLogger(t, ctrl)
logger.EXPECT().
IsEnabled(adaptors.LogLevel(logLevelDumpBody)).
Return(true)
transport := &LoggingTransport{
Logger: logger,
}
if transport.IsDumpBodyEnabled() != true {
t.Errorf("IsDumpBodyEnabled wants true")
}
}

View File

@@ -1,420 +0,0 @@
package integration_test
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/integration_test/httpdriver"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
)
// 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) {
timeout := 3 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
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)
}
}()
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
args []string
}{
"NoTLS": {},
"TLS": {
keyPair: keypair.Server,
args: []string{"--certificate-authority", keypair.Server.CACertPath},
},
} {
httpDriverOption := httpdriver.Option{
TLSConfig: tc.keyPair.TLSConfig,
BodyContains: "Authenticated",
}
t.Run(name, func(t *testing.T) {
t.Run("AuthCode", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ROPC", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
Username: "USER1",
Password: "PASS1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.Zero(t),
now: now,
stdout: &stdout,
args: append([]string{
"--username", "USER1",
"--password", "PASS1",
}, tc.args...),
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("TokenCacheLifecycle", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{})
defer sv.Shutdown(t, ctx)
t.Run("NoCache", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
RefreshToken: "REFRESH_TOKEN_1",
},
})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("Valid", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.Zero(t),
now: now,
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("Refresh", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(3 * time.Hour),
RefreshToken: "REFRESH_TOKEN_2",
},
})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(2 * time.Hour),
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(3*time.Hour))
})
t.Run("RefreshAgain", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_2",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(5 * time.Hour),
},
})
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(4 * time.Hour),
stdout: &stdout,
args: tc.args,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(5*time.Hour))
})
})
})
}
t.Run("PKCE", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
CodeChallengeMethod: "S256",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
CodeChallengeMethodsSupported: []string{"plain", "S256"},
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("TLSData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig, BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{"--certificate-authority-data", keypair.Server.CACertBase64},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "email profile openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("OpenURLAfterAuthentication", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "URL=https://example.com/success"}),
now: now,
stdout: &stdout,
args: []string{"--open-url-after-authentication", "https://example.com/success"},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("RedirectURLHostname", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://127.0.0.1:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{"--oidc-redirect-url-hostname", "127.0.0.1"},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("ExtraParams", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
ExtraParams: map[string]string{
"ttl": "86400",
"reauth": "false",
},
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{BodyContains: "Authenticated"}),
now: now,
stdout: &stdout,
args: []string{
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=false",
},
})
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
}
type getTokenConfig struct {
tokenCacheDir string
issuerURL string
httpDriver browser.Interface
stdout io.Writer
now time.Time
args []string
}
func runGetToken(t *testing.T, ctx context.Context, cfg getTokenConfig) {
cmd := di.NewCmdForHeadless(clock.Fake(cfg.now), os.Stdin, cfg.stdout, logger.New(t), cfg.httpDriver)
exitCode := cmd.Run(ctx, append([]string{
"kubelogin",
"get-token",
"--token-cache-dir", cfg.tokenCacheDir,
"--oidc-issuer-url", cfg.issuerURL,
"--oidc-client-id", "kubernetes",
"--listen-address", "127.0.0.1:0",
}, cfg.args...), "latest")
if exitCode != 0 {
t.Errorf("exit status wants 0 but %d", exitCode)
}
}
func assertCredentialPluginStdout(t *testing.T, stdout io.Reader, token string, expiry time.Time) {
var got clientauthenticationv1beta1.ExecCredential
if err := json.NewDecoder(stdout).Decode(&got); err != nil {
t.Errorf("could not decode json of the credential plugin: %s", err)
return
}
want := clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: token,
ExpirationTimestamp: &metav1.Time{Time: expiry},
},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("kubeconfig mismatch (-want +got):\n%s", diff)
}
}

View File

@@ -1,70 +0,0 @@
// Package httpdriver provides a test double of the browser.
package httpdriver
import (
"context"
"crypto/tls"
"io/ioutil"
"net/http"
"strings"
"testing"
)
type Option struct {
TLSConfig *tls.Config
BodyContains string
}
// New returns a client to simulate browser access.
func New(ctx context.Context, t *testing.T, o Option) *client {
return &client{ctx, t, o}
}
// Zero returns a client which call is not expected.
func Zero(t *testing.T) *zeroClient {
return &zeroClient{t}
}
type client struct {
ctx context.Context
t *testing.T
o Option
}
func (c *client) Open(url string) error {
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.o.TLSConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
c.t.Errorf("could not create a request: %s", err)
return nil
}
req = req.WithContext(c.ctx)
resp, err := client.Do(req)
if err != nil {
c.t.Errorf("could not send a request: %s", err)
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
c.t.Errorf("StatusCode wants 200 but %d", resp.StatusCode)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.t.Errorf("could not read body: %s", err)
return nil
}
body := string(b)
if !strings.Contains(body, c.o.BodyContains) {
c.t.Errorf("body should contain %s but was %s", c.o.BodyContains, body)
}
return nil
}
type zeroClient struct {
t *testing.T
}
func (c *zeroClient) Open(url string) error {
c.t.Errorf("unexpected function call Open(%s)", url)
return nil
}

View File

@@ -1,62 +0,0 @@
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}
}

View File

@@ -1,26 +0,0 @@
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)

View File

@@ -1,17 +0,0 @@
-----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-----

View File

@@ -1,17 +0,0 @@
-----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-----

View File

@@ -1,27 +0,0 @@
-----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-----

View File

@@ -1 +0,0 @@
9E12C7A1AF348811

View File

@@ -1,14 +0,0 @@
[ 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

View File

@@ -1,18 +0,0 @@
-----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-----

View File

@@ -1,17 +0,0 @@
-----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-----

View File

@@ -1,27 +0,0 @@
-----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-----

View File

@@ -1,4 +0,0 @@
apiVersion: v1
current-context: dummy
kind: Config
preferences: {}

View File

@@ -1,148 +0,0 @@
// Package handler provides a HTTP handler for the OpenID Connect Provider.
package handler
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"golang.org/x/xerrors"
)
func New(t *testing.T, provider Provider) *Handler {
return &Handler{t, provider}
}
// 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
provider Provider
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
wr := &responseWriterRecorder{w, 200}
err := h.serveHTTP(wr, r)
if err == nil {
h.t.Logf("%d %s %s", wr.statusCode, r.Method, r.RequestURI)
return
}
if errResp := new(ErrorResponse); xerrors.As(err, &errResp) {
h.t.Logf("400 %s %s: %s", r.Method, r.RequestURI, err)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(400)
e := json.NewEncoder(w)
if err := e.Encode(errResp); err != nil {
h.t.Errorf("idp/handler: could not write the response: %s", err)
}
return
}
h.t.Logf("500 %s %s: %s", r.Method, r.RequestURI, err)
http.Error(w, err.Error(), 500)
}
type responseWriterRecorder struct {
http.ResponseWriter
statusCode int
}
func (w *responseWriterRecorder) WriteHeader(statusCode int) {
w.ResponseWriter.WriteHeader(statusCode)
w.statusCode = statusCode
}
func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
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.provider.GetCertificates()
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(certificatesResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/auth":
q := r.URL.Query()
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"),
CodeChallenge: q.Get("code_challenge"),
CodeChallengeMethod: q.Get("code_challenge_method"),
RawQuery: q,
})
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
to := fmt.Sprintf("%s?state=%s&code=%s", redirectURI, state, code)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/token":
if err := r.ParseForm(); err != nil {
return xerrors.Errorf("could not parse the form: %w", err)
}
grantType := r.Form.Get("grant_type")
switch grantType {
case "authorization_code":
tokenResponse, err := h.provider.Exchange(TokenRequest{
Code: r.Form.Get("code"),
CodeVerifier: r.Form.Get("code_verifier"),
})
if err != nil {
return xerrors.Errorf("token request error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case "password":
// 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.provider.AuthenticatePassword(username, password, scope)
if err != nil {
return xerrors.Errorf("authentication error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
case "refresh_token":
// 12.1. Refresh Request
// https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken
refreshToken := r.Form.Get("refresh_token")
tokenResponse, err := h.provider.Refresh(refreshToken)
if err != nil {
return xerrors.Errorf("token refresh error: %w", err)
}
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(tokenResponse); err != nil {
return xerrors.Errorf("could not render json: %w", err)
}
default:
// 5.2. Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2
return &ErrorResponse{
Code: "invalid_grant",
Description: fmt.Sprintf("unknown grant_type %s", grantType),
}
}
default:
http.NotFound(w, r)
}
return nil
}

View File

@@ -1,87 +0,0 @@
package handler
import (
"fmt"
"net/url"
)
// 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 Provider interface {
Discovery() *DiscoveryResponse
GetCertificates() *CertificatesResponse
AuthenticateCode(req AuthenticationRequest) (code string, err error)
Exchange(req TokenRequest) (*TokenResponse, error)
AuthenticatePassword(username, password, scope string) (*TokenResponse, error)
Refresh(refreshToken string) (*TokenResponse, error)
}
type DiscoveryResponse struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
JwksURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
ScopesSupported []string `json:"scopes_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
type CertificatesResponse struct {
Keys []*CertificatesResponseKey `json:"keys"`
}
type CertificatesResponseKey struct {
Kty string `json:"kty"`
Alg string `json:"alg"`
Use string `json:"use"`
Kid string `json:"kid"`
N string `json:"n"`
E string `json:"e"`
}
// AuthenticationRequest represents a type of:
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
type AuthenticationRequest struct {
RedirectURI string
State string
Scope string // space separated string
Nonce string
CodeChallenge string
CodeChallengeMethod string
RawQuery url.Values
}
// TokenRequest represents a type of:
// https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest
type TokenRequest struct {
Code string
CodeVerifier string
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
IDToken string `json:"id_token"`
}
// ErrorResponse represents an error response described in the following section:
// 5.2 Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2
type ErrorResponse struct {
Code string `json:"error"`
Description string `json:"error_description"`
}
func (err *ErrorResponse) Error() string {
return fmt.Sprintf("%s(%s)", err.Code, err.Description)
}

View File

@@ -1,78 +0,0 @@
// Package http provides a http server running on localhost for testing.
package http
import (
"context"
"net"
"net/http"
"testing"
"github.com/int128/kubelogin/integration_test/keypair"
)
type Shutdowner interface {
Shutdown(t *testing.T, ctx context.Context)
}
type shutdowner struct {
s *http.Server
}
func (s *shutdowner) Shutdown(t *testing.T, ctx context.Context) {
if err := s.s.Shutdown(ctx); err != nil {
t.Errorf("could not shutdown the server: %s", err)
}
}
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
s := &http.Server{
Handler: h,
}
go func() {
err := s.Serve(l)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return url, &shutdowner{s}
}
func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) (string, *shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
s := &http.Server{
Handler: h,
}
go func() {
err := s.ServeTLS(l, k.CertPath, k.KeyPath)
if err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
return url, &shutdowner{s}
}
func newLocalhostListener(t *testing.T) (net.Listener, string) {
t.Helper()
l, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Could not create a listener: %s", err)
}
addr := l.Addr().String()
_, port, err := net.SplitHostPort(addr)
if err != nil {
t.Fatalf("Could not parse the address %s: %s", addr, err)
}
return l, port
}

View File

@@ -1,217 +0,0 @@
// Package oidcserver provides a stub of OpenID Connect provider.
package oidcserver
import (
"crypto/sha256"
"encoding/base64"
"math/big"
"strings"
"testing"
"time"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/oidcserver/handler"
"github.com/int128/kubelogin/integration_test/oidcserver/http"
"github.com/int128/kubelogin/pkg/testing/jwt"
"golang.org/x/xerrors"
)
type Server interface {
http.Shutdowner
IssuerURL() string
SetConfig(Config)
LastTokenResponse() *handler.TokenResponse
}
// Want represents a set of expected values.
type Want struct {
Scope string
RedirectURIPrefix string
CodeChallengeMethod string // optional
ExtraParams map[string]string // optional
Username string // optional
Password string // optional
RefreshToken string // optional
}
// Response represents a set of response values.
type Response struct {
IDTokenExpiry time.Time
RefreshToken string
RefreshError string // if set, Refresh() will return the error
CodeChallengeMethodsSupported []string // optional
}
// Config represents a configuration of the OpenID Connect provider.
type Config struct {
Want Want
Response Response
}
// New starts a HTTP server for the OpenID Connect provider.
func New(t *testing.T, k keypair.KeyPair, c Config) Server {
sv := server{Config: c, t: t}
sv.issuerURL, sv.Shutdowner = http.Start(t, handler.New(t, &sv), k)
return &sv
}
type server struct {
Config
http.Shutdowner
t *testing.T
issuerURL string
lastAuthenticationRequest *handler.AuthenticationRequest
lastTokenResponse *handler.TokenResponse
}
func (sv *server) IssuerURL() string {
return sv.issuerURL
}
func (sv *server) SetConfig(cfg Config) {
sv.Config = cfg
}
func (sv *server) LastTokenResponse() *handler.TokenResponse {
return sv.lastTokenResponse
}
func (sv *server) Discovery() *handler.DiscoveryResponse {
// based on https://accounts.google.com/.well-known/openid-configuration
return &handler.DiscoveryResponse{
Issuer: sv.issuerURL,
AuthorizationEndpoint: sv.issuerURL + "/auth",
TokenEndpoint: sv.issuerURL + "/token",
JwksURI: sv.issuerURL + "/certs",
UserinfoEndpoint: sv.issuerURL + "/userinfo",
RevocationEndpoint: sv.issuerURL + "/revoke",
ResponseTypesSupported: []string{"code id_token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
ScopesSupported: []string{"openid", "email", "profile"},
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
CodeChallengeMethodsSupported: sv.Config.Response.CodeChallengeMethodsSupported,
ClaimsSupported: []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
}
}
func (sv *server) GetCertificates() *handler.CertificatesResponse {
idTokenKeyPair := jwt.PrivateKey
return &handler.CertificatesResponse{
Keys: []*handler.CertificatesResponseKey{
{
Kty: "RSA",
Alg: "RS256",
Use: "sig",
Kid: "dummy",
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
N: base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
},
},
}
}
func (sv *server) AuthenticateCode(req handler.AuthenticationRequest) (code string, err error) {
if req.Scope != sv.Want.Scope {
sv.t.Errorf("scope wants `%s` but was `%s`", sv.Want.Scope, req.Scope)
}
if !strings.HasPrefix(req.RedirectURI, sv.Want.RedirectURIPrefix) {
sv.t.Errorf("redirectURI wants prefix `%s` but was `%s`", sv.Want.RedirectURIPrefix, req.RedirectURI)
}
if req.CodeChallengeMethod != sv.Want.CodeChallengeMethod {
sv.t.Errorf("code_challenge_method wants `%s` but was `%s`", sv.Want.CodeChallengeMethod, req.CodeChallengeMethod)
}
for k, v := range sv.Want.ExtraParams {
got := req.RawQuery.Get(k)
if got != v {
sv.t.Errorf("parameter %s wants `%s` but was `%s`", k, v, got)
}
}
sv.lastAuthenticationRequest = &req
return "YOUR_AUTH_CODE", nil
}
func (sv *server) Exchange(req handler.TokenRequest) (*handler.TokenResponse, error) {
if req.Code != "YOUR_AUTH_CODE" {
return nil, xerrors.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.Code)
}
if sv.lastAuthenticationRequest.CodeChallengeMethod == "S256" {
// https://tools.ietf.org/html/rfc7636#section-4.6
challenge := computeS256Challenge(req.CodeVerifier)
if challenge != sv.lastAuthenticationRequest.CodeChallenge {
sv.t.Errorf("pkce S256 challenge did not match (want %s but was %s)", sv.lastAuthenticationRequest.CodeChallenge, challenge)
}
}
resp := &handler.TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
RefreshToken: sv.Response.RefreshToken,
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
claims.Issuer = sv.issuerURL
claims.Subject = "SUBJECT"
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
claims.Audience = []string{"kubernetes"}
claims.Nonce = sv.lastAuthenticationRequest.Nonce
}),
}
sv.lastTokenResponse = resp
return resp, nil
}
func computeS256Challenge(verifier string) string {
c := sha256.Sum256([]byte(verifier))
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(c[:])
}
func (sv *server) AuthenticatePassword(username, password, scope string) (*handler.TokenResponse, error) {
if scope != sv.Want.Scope {
sv.t.Errorf("scope wants `%s` but was `%s`", sv.Want.Scope, scope)
}
if username != sv.Want.Username {
sv.t.Errorf("username wants `%s` but was `%s`", sv.Want.Username, username)
}
if password != sv.Want.Password {
sv.t.Errorf("password wants `%s` but was `%s`", sv.Want.Password, password)
}
resp := &handler.TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
RefreshToken: sv.Response.RefreshToken,
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
claims.Issuer = sv.issuerURL
claims.Subject = "SUBJECT"
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
claims.Audience = []string{"kubernetes"}
}),
}
sv.lastTokenResponse = resp
return resp, nil
}
func (sv *server) Refresh(refreshToken string) (*handler.TokenResponse, error) {
if refreshToken != sv.Want.RefreshToken {
sv.t.Errorf("refreshToken wants %s but was %s", sv.Want.RefreshToken, refreshToken)
}
if sv.Response.RefreshError != "" {
return nil, &handler.ErrorResponse{Code: "invalid_request", Description: sv.Response.RefreshError}
}
resp := &handler.TokenResponse{
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "YOUR_ACCESS_TOKEN",
RefreshToken: sv.Response.RefreshToken,
IDToken: jwt.EncodeF(sv.t, func(claims *jwt.Claims) {
claims.Issuer = sv.issuerURL
claims.Subject = "SUBJECT"
claims.IssuedAt = sv.Response.IDTokenExpiry.Add(-time.Hour).Unix()
claims.ExpiresAt = sv.Response.IDTokenExpiry.Unix()
claims.Audience = []string{"kubernetes"}
}),
}
sv.lastTokenResponse = resp
return resp, nil
}

View File

@@ -1,334 +0,0 @@
package integration_test
import (
"context"
"os"
"testing"
"time"
"github.com/int128/kubelogin/integration_test/httpdriver"
"github.com/int128/kubelogin/integration_test/keypair"
"github.com/int128/kubelogin/integration_test/kubeconfig"
"github.com/int128/kubelogin/integration_test/oidcserver"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/pkg/testing/clock"
"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) {
timeout := 3 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
for name, tc := range map[string]struct {
keyPair keypair.KeyPair
args []string
}{
"NoTLS": {},
"TLS": {
keyPair: keypair.Server,
},
} {
httpDriverOption := httpdriver.Option{
TLSConfig: tc.keyPair.TLSConfig,
BodyContains: "Authenticated",
}
t.Run(name, func(t *testing.T) {
t.Run("AuthCode", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthority: tc.keyPair.CACertPath,
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("ROPC", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
Username: "USER1",
Password: "PASS1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthority: tc.keyPair.CACertPath,
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.Zero(t),
now: now,
args: []string{
"--username", "USER1",
"--password", "PASS1",
},
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("TokenLifecycle", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, tc.keyPair, oidcserver.Config{})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthority: tc.keyPair.CACertPath,
})
defer os.Remove(kubeConfigFilename)
t.Run("NoToken", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
RefreshToken: "REFRESH_TOKEN_1",
},
})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_1",
})
})
t.Run("Valid", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.Zero(t),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_1",
})
})
t.Run("Refresh", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_1",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(3 * time.Hour),
RefreshToken: "REFRESH_TOKEN_2",
},
})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(2 * time.Hour),
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_2",
})
})
t.Run("RefreshAgain", func(t *testing.T) {
sv.SetConfig(oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
RefreshToken: "REFRESH_TOKEN_2",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(5 * time.Hour),
},
})
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
now: now.Add(4 * time.Hour),
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: "REFRESH_TOKEN_2",
})
})
})
})
}
t.Run("TLSData", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.Server, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
IDPCertificateAuthorityData: keypair.Server.CACertBase64,
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{TLSConfig: keypair.Server.TLSConfig}),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("env_KUBECONFIG", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
})
defer os.Remove(kubeConfigFilename)
setenv(t, "KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
defer unsetenv(t, "KUBECONFIG")
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
t.Run("ExtraScopes", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
defer cancel()
sv := oidcserver.New(t, keypair.None, oidcserver.Config{
Want: oidcserver.Want{
Scope: "profile groups openid",
RedirectURIPrefix: "http://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
ExtraScopes: "profile,groups",
})
defer os.Remove(kubeConfigFilename)
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpdriver.Option{}),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
IDToken: sv.LastTokenResponse().IDToken,
RefreshToken: sv.LastTokenResponse().RefreshToken,
})
})
}
type standaloneConfig struct {
issuerURL string
kubeConfigFilename string
httpDriver browser.Interface
now time.Time
args []string
}
func runStandalone(t *testing.T, ctx context.Context, cfg standaloneConfig) {
cmd := di.NewCmdForHeadless(clock.Fake(cfg.now), os.Stdin, os.Stdout, logger.New(t), cfg.httpDriver)
exitCode := cmd.Run(ctx, append([]string{
"kubelogin",
"--kubeconfig", cfg.kubeConfigFilename,
"--listen-address", "127.0.0.1:0",
}, cfg.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)
}
}

80
kubeconfig/auth.go Normal file
View File

@@ -0,0 +1,80 @@
package kubeconfig
import (
"strings"
"github.com/pkg/errors"
"k8s.io/client-go/tools/clientcmd/api"
)
// FindOIDCAuthProvider returns the current OIDC authProvider.
// If the context, auth-info or auth-provider does not exist, this returns an error.
// If auth-provider is not "oidc", this returns an error.
func FindOIDCAuthProvider(config *api.Config) (*OIDCAuthProvider, error) {
context := config.Contexts[config.CurrentContext]
if context == nil {
return nil, errors.Errorf("context %s does not exist", config.CurrentContext)
}
authInfo := config.AuthInfos[context.AuthInfo]
if authInfo == nil {
return nil, errors.Errorf("auth-info %s does not exist", context.AuthInfo)
}
if authInfo.AuthProvider == nil {
return nil, errors.Errorf("auth-provider is not set")
}
if authInfo.AuthProvider.Name != "oidc" {
return nil, errors.Errorf("auth-provider name is %s but must be oidc", authInfo.AuthProvider.Name)
}
return (*OIDCAuthProvider)(authInfo.AuthProvider), nil
}
// OIDCAuthProvider represents OIDC configuration in the kubeconfig.
type OIDCAuthProvider api.AuthProviderConfig
// IDPIssuerURL returns the idp-issuer-url.
func (c *OIDCAuthProvider) IDPIssuerURL() string {
return c.Config["idp-issuer-url"]
}
// ClientID returns the client-id.
func (c *OIDCAuthProvider) ClientID() string {
return c.Config["client-id"]
}
// ClientSecret returns the client-secret.
func (c *OIDCAuthProvider) ClientSecret() string {
return c.Config["client-secret"]
}
// IDPCertificateAuthority returns the idp-certificate-authority.
func (c *OIDCAuthProvider) IDPCertificateAuthority() string {
return c.Config["idp-certificate-authority"]
}
// IDPCertificateAuthorityData returns the idp-certificate-authority-data.
func (c *OIDCAuthProvider) IDPCertificateAuthorityData() string {
return c.Config["idp-certificate-authority-data"]
}
// ExtraScopes returns the extra-scopes.
func (c *OIDCAuthProvider) ExtraScopes() []string {
if c.Config["extra-scopes"] == "" {
return []string{}
}
return strings.Split(c.Config["extra-scopes"], ",")
}
// IDToken returns the id-token.
func (c *OIDCAuthProvider) IDToken() string {
return c.Config["id-token"]
}
// SetIDToken replaces the id-token.
func (c *OIDCAuthProvider) SetIDToken(idToken string) {
c.Config["id-token"] = idToken
}
// SetRefreshToken replaces the refresh-token.
func (c *OIDCAuthProvider) SetRefreshToken(refreshToken string) {
c.Config["refresh-token"] = refreshToken
}

View File

@@ -1,27 +1,15 @@
class Kubelogin < Formula
desc "A kubectl plugin for Kubernetes OpenID Connect authentication"
homepage "https://github.com/int128/kubelogin"
baseurl = "https://github.com/int128/kubelogin/releases/download"
url "https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip"
version "{{ env "VERSION" }}"
if OS.mac?
kernel = "darwin"
sha256 "{{ sha256 .darwin_amd64_archive }}"
elsif OS.linux?
kernel = "linux"
sha256 "{{ sha256 .linux_amd64_archive }}"
end
url baseurl + "/#{version}/kubelogin_#{kernel}_amd64.zip"
sha256 "{{ .darwin_amd64_zip_sha256 }}"
def install
bin.install "kubelogin" => "kubelogin"
ln_s bin/"kubelogin", bin/"kubectl-oidc_login"
end
test do
system "#{bin}/kubelogin -h"
system "#{bin}/kubectl-oidc_login -h"
end
end

10
main.go
View File

@@ -2,13 +2,19 @@ package main
import (
"context"
"log"
"os"
"github.com/int128/kubelogin/pkg/di"
"github.com/int128/kubelogin/adaptors/interfaces"
"github.com/int128/kubelogin/di"
)
var version = "HEAD"
func main() {
os.Exit(di.NewCmd().Run(context.Background(), os.Args, version))
if err := di.Invoke(func(cmd adaptors.Cmd) {
os.Exit(cmd.Run(context.Background(), os.Args, version))
}); err != nil {
log.Fatalf("Error: %s", err)
}
}

57
oidc-login.yaml Normal file
View File

@@ -0,0 +1,57 @@
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: oidc-login
spec:
shortDescription: Login for OpenID Connect authentication
description: |
This plugin gets a token from the OIDC provider and writes it to the kubeconfig.
Just run:
% kubectl oidc-login
It opens the browser and you can log in to the provider.
After authentication, it gets an ID token and refresh token and writes them to the kubeconfig.
caveats: |
You need to setup the following components:
* OIDC provider
* Kubernetes API server
* Role for your group or user
* kubectl authentication
See https://github.com/int128/kubelogin for more.
homepage: https://github.com/int128/kubelogin
version: {{ env "VERSION" }}
platforms:
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
sha256: "{{ .linux_amd64_zip_sha256 }}"
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: "{{ .darwin_amd64_zip_sha256 }}"
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: "{{ .windows_amd64_zip_sha256 }}"
bin: kubelogin.exe
files:
- from: "kubelogin.exe"
to: "."
selector:
matchLabels:
os: windows
arch: amd64

View File

@@ -1,33 +0,0 @@
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)
}

View File

@@ -1,47 +0,0 @@
// 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)
}

View File

@@ -1,71 +0,0 @@
// 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
}

View File

@@ -1,58 +0,0 @@
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)
}

View File

@@ -1,74 +0,0 @@
// 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)
}

View File

@@ -1,28 +0,0 @@
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
%.key:
openssl genrsa -out $@ 1024
%.csr: %.key
openssl req \
-new \
-key $< \
-subj "/CN=Hello" \
-days 3650 \
-out $@
openssl req -noout -text -in $@
%.crt: %.csr %.key
openssl x509 -req \
-signkey $*.key \
-in $*.csr \
-days 3650 \
-out $@
openssl x509 -text -in $@
%.crt.base64: %.crt
base64 -i $< -o $@

View File

@@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAQACCQCDf7Inwu3vkzANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVI
ZWxsbzAeFw0xOTA2MjQxMDQ0NDhaFw0yOTA2MjExMDQ0NDhaMBAxDjAMBgNVBAMM
BUhlbGxvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCitZv5Go16nuHDRa2u
nT5m1Q9tkr668pnhcP0TkyjD+oEB0lUz2SJEZEvOd1XVRRrPMSXrtybo9p0TqSGp
Ig1gORWis/j/IR1sYdFutLKhtp6k1HvUiNosdO/K8K/AbO4QPWTGBAcqg//QkMKd
ccgLY2PYczK/t8+6C7JYEHe5AwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACkPsyme
JFlj75fO54NH5WXZxBtoY7kV3yd5oO88BngE8ittaHuauQkkw/sC5x733SsJlPlF
trah4CDMjq5d/okIbIJFKe7NGLi82f9zJ+o1fjDp97UvZHC0zhUx+RiEu3iZRfYM
31Ht7QG63V5ScV3Zmi1nzfQc4jn8d40kXXcn
-----END CERTIFICATE-----

View File

@@ -1 +0,0 @@
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJsekNDQVFBQ0NRQ0RmN0lud3Uzdmt6QU5CZ2txaGtpRzl3MEJBUVVGQURBUU1RNHdEQVlEVlFRRERBVkkKWld4c2J6QWVGdzB4T1RBMk1qUXhNRFEwTkRoYUZ3MHlPVEEyTWpFeE1EUTBORGhhTUJBeERqQU1CZ05WQkFNTQpCVWhsYkd4dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRQ2l0WnY1R28xNm51SERSYTJ1Cm5UNW0xUTl0a3I2NjhwbmhjUDBUa3lqRCtvRUIwbFV6MlNKRVpFdk9kMVhWUlJyUE1TWHJ0eWJvOXAwVHFTR3AKSWcxZ09SV2lzL2ovSVIxc1lkRnV0TEtodHA2azFIdlVpTm9zZE8vSzhLL0FiTzRRUFdUR0JBY3FnLy9Ra01LZApjY2dMWTJQWWN6Sy90OCs2QzdKWUVIZTVBd0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFDa1BzeW1lCkpGbGo3NWZPNTROSDVXWFp4QnRvWTdrVjN5ZDVvTzg4Qm5nRThpdHRhSHVhdVFra3cvc0M1eDczM1NzSmxQbEYKdHJhaDRDRE1qcTVkL29rSWJJSkZLZTdOR0xpODJmOXpKK28xZmpEcDk3VXZaSEMwemhVeCtSaUV1M2laUmZZTQozMUh0N1FHNjNWNVNjVjNabWkxbnpmUWM0am44ZDQwa1hYY24KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

View File

@@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAQACCQCuudlGZuJvODANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVI
ZWxsbzAeFw0xOTA2MjQxMDQ0NDhaFw0yOTA2MjExMDQ0NDhaMBAxDjAMBgNVBAMM
BUhlbGxvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHnrC3q5aCXhULGTTg
w7psUbrH3gpHEExxlw6Zj+UBZHFhxOccGYfHPvqKwfRAKfqkP6VzLdYsfF0fuMOX
ZzFk2hB1eAdl2dsFIn4hMll+jDdo9x+7NKvAXgsFF174ZMVTW26aAME8s4OrNuZT
Fdrp7byuUkwUbSzDC/B/ct9MFQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAGXsF0IA
yP3g1UTLpld2P38dvMXLGN6gwn0S0oh7AQMckJ35yh8CN/2rAkBVujyvGILLhh2/
teoIjM2BcZsrsKZ+Jkr177fRIunsd7a+v18M/3/pVvxPZdnztXspycxIacd7yVbG
5wjN+X7rkoBLhd+BT9+W9O/i+Cu7K89JOO64
-----END CERTIFICATE-----

View File

@@ -1 +0,0 @@
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJsekNDQVFBQ0NRQ3V1ZGxHWnVKdk9EQU5CZ2txaGtpRzl3MEJBUVVGQURBUU1RNHdEQVlEVlFRRERBVkkKWld4c2J6QWVGdzB4T1RBMk1qUXhNRFEwTkRoYUZ3MHlPVEEyTWpFeE1EUTBORGhhTUJBeERqQU1CZ05WQkFNTQpCVWhsYkd4dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRREhuckMzcTVhQ1hoVUxHVFRnCnc3cHNVYnJIM2dwSEVFeHhsdzZaaitVQlpIRmh4T2NjR1lmSFB2cUt3ZlJBS2Zxa1A2VnpMZFlzZkYwZnVNT1gKWnpGazJoQjFlQWRsMmRzRkluNGhNbGwrakRkbzl4KzdOS3ZBWGdzRkYxNzRaTVZUVzI2YUFNRThzNE9yTnVaVApGZHJwN2J5dVVrd1ViU3pEQy9CL2N0OU1GUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFHWHNGMElBCnlQM2cxVVRMcGxkMlAzOGR2TVhMR042Z3duMFMwb2g3QVFNY2tKMzV5aDhDTi8yckFrQlZ1anl2R0lMTGhoMi8KdGVvSWpNMkJjWnNyc0taK0prcjE3N2ZSSXVuc2Q3YSt2MThNLzMvcFZ2eFBaZG56dFhzcHljeElhY2Q3eVZiRwo1d2pOK1g3cmtvQkxoZCtCVDkrVzlPL2krQ3U3Szg5Sk9PNjQKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

View File

@@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBlzCCAQACCQDyFQsG5rDcJTANBgkqhkiG9w0BAQUFADAQMQ4wDAYDVQQDDAVI
ZWxsbzAeFw0xOTA2MjQxMDQ0NDhaFw0yOTA2MjExMDQ0NDhaMBAxDjAMBgNVBAMM
BUhlbGxvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUBsXHnAjPL3ejyNRs
INI0cp4Sv4HzgXmL6nypzTdEOT3UcqfvZYj3dr4FWZytxb6XgvyvIzoV++GS22cf
arXwwv0Z6CWiJXI+WQFdsQRQoAt4ucIa046b18p6mCiHfaH98aCOq9K3sxTfNOm3
kWAi6oFzB5C+6HalQ+rWFSVWHQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBALQfsBMR
pxD9vzTmhw+rc0HKei9QMViIC3KYPdzvCCe0lMjrWzvcmTtUyCNJm2J2GwBVfyok
zeUskYjinppBy/ZmzpWTeqTLOoeozgAh/Jgya5cPh01BP+pPFYmcQ5wOZHK5PPSP
jvfqMeYs8TjXJRjdBKcMuZAN/8g2Ubtn+QbM
-----END CERTIFICATE-----

View File

@@ -1 +0,0 @@
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJsekNDQVFBQ0NRRHlGUXNHNXJEY0pUQU5CZ2txaGtpRzl3MEJBUVVGQURBUU1RNHdEQVlEVlFRRERBVkkKWld4c2J6QWVGdzB4T1RBMk1qUXhNRFEwTkRoYUZ3MHlPVEEyTWpFeE1EUTBORGhhTUJBeERqQU1CZ05WQkFNTQpCVWhsYkd4dk1JR2ZNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0R05BRENCaVFLQmdRRFVCc1hIbkFqUEwzZWp5TlJzCklOSTBjcDRTdjRIemdYbUw2bnlwelRkRU9UM1VjcWZ2WllqM2RyNEZXWnl0eGI2WGd2eXZJem9WKytHUzIyY2YKYXJYd3d2MFo2Q1dpSlhJK1dRRmRzUVJRb0F0NHVjSWEwNDZiMThwNm1DaUhmYUg5OGFDT3E5SzNzeFRmTk9tMwprV0FpNm9GekI1Qys2SGFsUStyV0ZTVldIUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRHQkFMUWZzQk1SCnB4RDl2elRtaHcrcmMwSEtlaTlRTVZpSUMzS1lQZHp2Q0NlMGxNanJXenZjbVR0VXlDTkptMkoyR3dCVmZ5b2sKemVVc2tZamlucHBCeS9abXpwV1RlcVRMT29lb3pnQWgvSmd5YTVjUGgwMUJQK3BQRlltY1E1d09aSEs1UFBTUApqdmZxTWVZczhUalhKUmpkQktjTXVaQU4vOGcyVWJ0bitRYk0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=

View File

@@ -1,24 +0,0 @@
// Package clock provides the system clock.
package clock
import (
"time"
"github.com/google/wire"
)
var Set = wire.NewSet(
wire.Struct(new(Real), "*"),
wire.Bind(new(Interface), new(*Real)),
)
type Interface interface {
Now() time.Time
}
type Real struct{}
// Now returns the current time.
func (c *Real) Now() time.Time {
return time.Now()
}

View File

@@ -1,87 +0,0 @@
package cmd
import (
"fmt"
"strings"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
SkipOpenBrowser bool
OpenURLAfterAuthentication string
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) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("Authorization grant type to use. One of (%s)", allGrantType))
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "[authcode] Address to bind to the local server. If multiple addresses are set, it will try binding in order")
//TODO: remove the deprecated flag
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "[authcode] deprecated: port to bind to the local server")
if err := f.MarkDeprecated("listen-port", "use --listen-address instead"); err != nil {
panic(err)
}
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "[authcode] Do not open the browser automatically")
f.StringVar(&o.OpenURLAfterAuthentication, "open-url-after-authentication", "", "[authcode] If set, open the URL in the browser after authentication")
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "[authcode] Hostname of the redirect URL")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "[authcode, authcode-keyboard] Extra query parameters to send with an authentication request")
f.StringVar(&o.Username, "username", "", "[password] Username for resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "[password] Password for resource owner password credentials grant")
}
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeBrowserOption = &authcode.BrowserOption{
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
OpenURLAfterAuthentication: o.OpenURLAfterAuthentication,
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authcode.KeyboardOption{
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &ropc.Option{
Username: o.Username,
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}

View File

@@ -1,68 +0,0 @@
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
}

View File

@@ -1,382 +0,0 @@
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/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"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{
AuthCodeBrowserOption: &authcode.BrowserOption{
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{
AuthCodeBrowserOption: &authcode.BrowserOption{
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{
AuthCodeBrowserOption: &authcode.BrowserOption{
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",
"--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",
"--open-url-after-authentication", "https://example.com/success.html",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "localhost",
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"--grant-type", "authcode-keyboard",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authcode.KeyboardOption{},
},
},
},
"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: &ropc.Option{
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: &ropc.Option{
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{
AuthCodeBrowserOption: &authcode.BrowserOption{
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",
"--open-url-after-authentication", "https://example.com/success.html",
"--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{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
OpenURLAfterAuthentication: "https://example.com/success.html",
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: &authcode.KeyboardOption{
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: &ropc.Option{
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: &ropc.Option{
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)
}
})
})
}

View File

@@ -1,79 +0,0 @@
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
TokenCacheDir string
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *getTokenOptions) addFlags(f *pflag.FlagSet) {
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.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for token cache")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(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.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.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
},
}
c.Flags().SortFlags = false
o.addFlags(c.Flags())
return c
}

View File

@@ -1,75 +0,0 @@
package cmd
import (
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
const rootDescription = `Log in to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
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
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *rootOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "Name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "Name of the kubeconfig user to use. Prior to --context")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
}
type Root struct {
Standalone standalone.Interface
Logger logger.Interface
}
func (cmd *Root) New() *cobra.Command {
var o rootOptions
c := &cobra.Command{
Use: "kubelogin",
Short: "Log in to the OpenID Connect provider",
Long: rootDescription,
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.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
}
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
return xerrors.Errorf("login: %w", err)
}
return nil
},
}
c.Flags().SortFlags = false
o.addFlags(c.Flags())
cmd.Logger.AddFlags(c.PersistentFlags())
return c
}

View File

@@ -1,70 +0,0 @@
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
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *setupOptions) addFlags(f *pflag.FlagSet) {
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")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(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.tlsOptions.CACertFilename,
CACertData: o.tlsOptions.CACertData,
SkipTLSVerify: o.tlsOptions.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
},
}
c.Flags().SortFlags = false
o.addFlags(c.Flags())
return c
}

View File

@@ -1,15 +0,0 @@
package cmd
import "github.com/spf13/pflag"
type tlsOptions struct {
CACertFilename string
CACertData string
SkipTLSVerify bool
}
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded cert for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
}

View File

@@ -1,53 +0,0 @@
// Package credentialpluginwriter provides a writer for a credential plugin.
package credentialpluginwriter
import (
"encoding/json"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/xerrors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "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 {
Stdout stdio.Stdout
}
// Write writes the ExecCredential to standard output for kubectl.
func (w *Writer) Write(out Output) error {
ec := &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
Kind: "ExecCredential",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
Token: out.Token,
ExpirationTimestamp: &metav1.Time{Time: out.Expiry},
},
}
e := json.NewEncoder(w.Stdout)
if err := e.Encode(ec); err != nil {
return xerrors.Errorf("could not write the ExecCredential: %w", err)
}
return nil
}

View File

@@ -1,48 +0,0 @@
// 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)
}

View File

@@ -1,45 +0,0 @@
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
}

View File

@@ -1,80 +0,0 @@
package kubeconfig
import (
"strings"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
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 the kubeconfig: %w", err)
}
auth, err := findCurrentAuthProvider(config, contextName, userName)
if err != nil {
return nil, xerrors.Errorf("could not find the current auth provider: %w", err)
}
return auth, nil
}
func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules := clientcmd.NewDefaultClientConfigLoadingRules()
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, xerrors.Errorf("load error: %w", err)
}
return config, err
}
// findCurrentAuthProvider resolves the current auth provider.
// 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 ContextName, userName UserName) (*AuthProvider, error) {
if userName == "" {
if contextName == "" {
contextName = ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, xerrors.Errorf("context %s does not exist", contextName)
}
userName = UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
return nil, xerrors.Errorf("user %s does not exist", userName)
}
if userNode.AuthProvider == nil {
return nil, xerrors.New("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return nil, xerrors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
}
if userNode.AuthProvider.Config == nil {
return nil, xerrors.New("auth-provider.config is missing")
}
m := userNode.AuthProvider.Config
var extraScopes []string
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,
IDPIssuerURL: m["idp-issuer-url"],
ClientID: m["client-id"],
ClientSecret: m["client-secret"],
IDPCertificateAuthority: m["idp-certificate-authority"],
IDPCertificateAuthorityData: m["idp-certificate-authority-data"],
ExtraScopes: extraScopes,
IDToken: m["id-token"],
RefreshToken: m["refresh-token"],
}, nil
}

Some files were not shown because too many files have changed in this diff Show More