Compare commits

..

1 Commits

Author SHA1 Message Date
Hidetake Iwata
d7554b6d90 Move to CircleCI macOS build 2020-06-12 14:08:51 +09:00
162 changed files with 4178 additions and 7011 deletions

60
.circleci/config.yml Normal file
View File

@@ -0,0 +1,60 @@
version: 2.1
jobs:
test:
docker:
- image: cimg/go:1.14.4
steps:
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
- checkout
- restore_cache:
keys:
- go-sum-{{ checksum "go.sum" }}
- run: make check
- run: bash <(curl -s https://codecov.io/bash)
- save_cache:
key: go-sum-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
- store_artifacts:
path: gotest.log
crossbuild:
macos:
xcode: 11.5.0
steps:
- run: |
curl -sSfL https://dl.google.com/go/go1.14.4.darwin-amd64.tar.gz | tar -C /tmp -xz
echo 'export PATH="$PATH:/tmp/go/bin:$HOME/go/bin"' >> $BASH_ENV
- checkout
- restore_cache:
keys:
- go-macos-{{ checksum "go.sum" }}
- run:
command: go get -v github.com/int128/goxzst github.com/int128/ghcp
working_directory: .circleci
- run: make dist
- run: |
if [ "$CIRCLE_TAG" ]; then
make release
fi
- save_cache:
key: go-macos-{{ checksum "go.sum" }}
paths:
- ~/go/pkg
workflows:
version: 2
build:
jobs:
- test:
filters:
tags:
only: /.*/
- crossbuild:
context: open-source
requires:
- test
filters:
tags:
only: /.*/

3
.circleci/go.mod Normal file
View File

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

View File

@@ -1,20 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
## Describe the issue
A clear and concise description of what the issue is.
## To reproduce
A console log or steps to reproduce the issue.
## Your environment
- OS: e.g. macOS
- kubelogin version: e.g. v1.19
- kubectl version: e.g. v1.19
- OpenID Connect provider: e.g. Google

View File

@@ -1,15 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
## Purpose of the feature (why)
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
A clear and concise description of what you want to happen.
## Your idea (how)
A clear and concise description of any alternative solutions or features you've considered.

View File

@@ -1,20 +0,0 @@
---
name: Question
about: Feel free to ask a question
title: ''
labels: question
assignees: ''
---
## Describe the question
A clear and concise description of what the issue is.
## To reproduce
A console log or steps to reproduce the issue.
## Your environment
- OS: e.g. macOS
- kubelogin version: e.g. v1.19
- kubectl version: e.g. v1.19
- OpenID Connect provider: e.g. Google

View File

@@ -1,6 +0,0 @@
{
"extends": [
"github>int128/renovate-base",
"github>int128/go-actions",
],
}

30
.github/workflows/acceptance-test.yaml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: acceptance-test
on: [push]
jobs:
build:
name: test
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/software-installed-on-github-hosted-runners#ubuntu-1804-lts
runs-on: ubuntu-18.04
steps:
- uses: actions/setup-go@v1
with:
go-version: 1.14.1
id: go
- uses: actions/checkout@v1
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
restore-keys: |
go-
# https://kind.sigs.k8s.io/docs/user/quick-start/
- run: |
wget -q -O ./kind "https://github.com/kubernetes-sigs/kind/releases/download/v0.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 install -y libnss3-tools
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
- run: make -C acceptance_test -j3 setup
- run: make -C acceptance_test test

View File

@@ -1,52 +0,0 @@
name: docker
on:
pull_request:
branches:
- master
paths:
- .github/workflows/docker.yaml
- pkg/**
- go.*
- Dockerfile
- Makefile
push:
branches:
- master
paths:
- .github/workflows/docker.yaml
- pkg/**
- go.*
- Dockerfile
- Makefile
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/metadata-action@v4
id: metadata
with:
images: ghcr.io/${{ github.repository }}
- uses: int128/docker-build-cache-config-action@v1
id: cache
with:
image: ghcr.io/${{ github.repository }}/cache
- uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/build-push-action@v3
with:
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
cache-from: ${{ steps.cache.outputs.cache-from }}
cache-to: ${{ steps.cache.outputs.cache-to }}
platforms: linux/amd64,linux/arm64,linux/ppc64le

View File

@@ -1,42 +0,0 @@
name: go
on:
push:
branches:
- master
paths:
- .github/workflows/go.yaml
- pkg/**
- go.*
tags:
- v*
pull_request:
branches:
- master
paths:
- .github/workflows/go.yaml
- pkg/**
- go.*
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: int128/go-actions/setup@v1
with:
go-version: 1.18.5
- uses: golangci/golangci-lint-action@v3
with:
version: v1.48.0
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: int128/go-actions/setup@v1
with:
go-version: 1.18.5
- run: go test -v -race ./...

View File

@@ -1,74 +0,0 @@
name: release
on:
push:
branches:
- master
paths:
- .github/workflows/release.yaml
- pkg/**
- go.*
tags:
- v*
pull_request:
branches:
- master
paths:
- .github/workflows/release.yaml
- pkg/**
- go.*
jobs:
build:
strategy:
matrix:
platform:
- runs-on: ubuntu-latest
GOOS: linux
GOARCH: amd64
CGO_ENABLED: 0 # https://github.com/int128/kubelogin/issues/567
- runs-on: ubuntu-latest
GOOS: linux
GOARCH: arm64
- runs-on: ubuntu-latest
GOOS: linux
GOARCH: arm
- runs-on: ubuntu-latest
GOOS: linux
GOARCH: ppc64le
- runs-on: macos-latest
GOOS: darwin
GOARCH: amd64
CGO_ENABLED: 1 # https://github.com/int128/kubelogin/issues/249
- runs-on: macos-latest
GOOS: darwin
GOARCH: arm64
CGO_ENABLED: 1 # https://github.com/int128/kubelogin/issues/762
- runs-on: windows-latest
GOOS: windows
GOARCH: amd64
runs-on: ${{ matrix.platform.runs-on }}
env:
GOOS: ${{ matrix.platform.GOOS }}
GOARCH: ${{ matrix.platform.GOARCH }}
CGO_ENABLED: ${{ matrix.platform.CGO_ENABLED }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: int128/go-actions/setup@v1
with:
go-version: 1.18.5
- run: go build -ldflags "-X main.version=${GITHUB_REF##*/}"
- uses: int128/go-actions/release@v1
with:
binary: kubelogin
publish:
if: startswith(github.ref, 'refs/tags/')
needs:
- build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: rajatjindal/krew-release-bot@v0.0.43

View File

@@ -1,41 +0,0 @@
name: system-test
on:
pull_request:
branches:
- master
paths:
- .github/workflows/system-test.yaml
- system_test/**
- pkg/**
- go.*
push:
branches:
- master
paths:
- .github/workflows/system-test.yaml
- system_test/**
- pkg/**
- go.*
jobs:
system-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: int128/go-actions/setup@v1
with:
go-version: 1.18.5
# for certutil
# https://packages.ubuntu.com/xenial/libnss3-tools
- run: sudo apt update
- run: sudo apt install -y libnss3-tools
- run: mkdir -p ~/.pki/nssdb
- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts
- run: make -C system_test -j3
- run: make -C system_test logs
if: always()

4
.gitignore vendored
View File

@@ -2,9 +2,9 @@
/acceptance_test/output/
/dist/output
/coverage.out
/gotest.log
/kubelogin
/kubectl-oidc_login
/kubelogin_*.zip
/kubelogin_*.zip.sha256

View File

@@ -1,62 +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: {{ .TagName }}
platforms:
- bin: kubelogin
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_linux_amd64.zip" .TagName }}
selector:
matchLabels:
os: linux
arch: amd64
- bin: kubelogin
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_linux_arm64.zip" .TagName }}
selector:
matchLabels:
os: linux
arch: arm64
- bin: kubelogin
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_linux_arm.zip" .TagName }}
selector:
matchLabels:
os: linux
arch: arm
- bin: kubelogin
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_darwin_amd64.zip" .TagName }}
selector:
matchLabels:
os: darwin
arch: amd64
- bin: kubelogin
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_darwin_arm64.zip" .TagName }}
selector:
matchLabels:
os: darwin
arch: arm64
- bin: kubelogin.exe
{{ addURIAndSha "https://github.com/int128/kubelogin/releases/download/{{ .TagName }}/kubelogin_windows_amd64.zip" .TagName }}
selector:
matchLabels:
os: windows
arch: amd64

View File

@@ -1,12 +0,0 @@
FROM golang:1.18 as builder
WORKDIR /builder
COPY go.* .
RUN go mod download
COPY main.go .
COPY pkg pkg
RUN go build
FROM gcr.io/distroless/base-debian10
COPY --from=builder /builder/kubelogin /
ENTRYPOINT ["/kubelogin"]

49
Makefile Normal file
View File

@@ -0,0 +1,49 @@
# 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)
all: $(TARGET)
$(TARGET): $(wildcard **/*.go)
go build -o $@ -ldflags "$(LDFLAGS)"
.PHONY: check
check:
golangci-lint run
go test -v -race -cover -coverprofile=coverage.out ./... > gotest.log
.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
.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
.PHONY: clean
clean:
-rm $(TARGET)
-rm -r dist/output/
-rm coverage.out gotest.log

255
README.md
View File

@@ -1,10 +1,10 @@
# kubelogin [![go](https://github.com/int128/kubelogin/actions/workflows/go.yaml/badge.svg)](https://github.com/int128/kubelogin/actions/workflows/go.yaml) [![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) ![acceptance-test](https://github.com/int128/kubelogin/workflows/acceptance-test/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/int128/kubelogin)](https://goreportcard.com/report/github.com/int128/kubelogin)
This is a kubectl plugin for [Kubernetes OpenID Connect (OIDC) authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens), also known as `kubectl oidc-login`.
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">
<img alt="screencast" src="https://user-images.githubusercontent.com/321266/70971501-7bcebc80-20e4-11ea-8afc-539dcaea0aa8.gif" width="652" height="455">
Kubelogin is designed to run as a [client-go credential plugin](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins).
When you run kubectl, kubelogin opens the browser and you can log in to the provider.
@@ -18,7 +18,7 @@ Take a look at the diagram:
### Setup
Install the latest release from [Homebrew](https://brew.sh/), [Krew](https://github.com/kubernetes-sigs/krew), [Chocolatey](https://chocolatey.org/packages/kubelogin) or [GitHub Releases](https://github.com/int128/kubelogin/releases).
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).
```sh
# Homebrew (macOS and Linux)
@@ -26,13 +26,8 @@ brew install int128/kubelogin/kubelogin
# Krew (macOS, Linux, Windows and ARM)
kubectl krew install oidc-login
# Chocolatey (Windows)
choco install kubelogin
```
If you install via GitHub releases, you need to put the `kubelogin` binary on your path under the name `kubectl-oidc_login` so that the [kubectl plugin mechanism](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/) can find it when you invoke `kubectl oidc-login`. The other install methods do this for you.
You need to set up the OIDC provider, cluster role binding, Kubernetes API server and kubeconfig.
The kubeconfig looks like:
@@ -51,7 +46,7 @@ users:
- --oidc-client-secret=YOUR_CLIENT_SECRET
```
See [setup guide](docs/setup.md) for more.
See [the setup guide](docs/setup.md) for more.
### Run
@@ -63,11 +58,11 @@ kubectl get pods
```
Kubectl executes kubelogin before calling the Kubernetes APIs.
Kubelogin automatically opens the browser, and you can log in to the provider.
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 kubectl then calls the Kubernetes APIs with these credentials.
After authentication, kubelogin returns the credentials to kubectl and finally kubectl calls the Kubernetes APIs with the credential.
```
% kubectl get pods
@@ -80,30 +75,199 @@ 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 re-authentication (you will have to login via browser again).
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 ask you to login via browser again if the token cache file does not exist i.e., it starts with a clean slate
Kubelogin will perform authentication if the token cache file does not exist.
You can dump claims of an ID token by `setup` command.
You can dump the claims of token by passing `-v1` option.
```console
% kubectl oidc-login setup --oidc-issuer-url https://accounts.google.com --oidc-client-id REDACTED --oidc-client-secret REDACTED
...
You got a token with the following claims:
{
```
I0221 21:54:08.151850 28231 get_token.go:104] you got a token: {
"sub": "********",
"iss": "https://accounts.google.com",
"aud": "********",
...
"iat": 1582289639,
"exp": 1582293239,
"jti": "********",
"nonce": "********",
"at_hash": "********"
}
```
You can increase the log level by `-v1` option.
## 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
--certificate-authority string Path to a cert file for the certificate authority
--certificate-authority-data string Base64 encoded data for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--listen-port ints (Deprecated: use --listen-address)
--skip-open-browser If true, it does not open the browser on authentication
--oidc-redirect-url-hostname string Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
-h, --help help for get-token
Global Flags:
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
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`.
```yaml
- --oidc-extra-scope=email
- --oidc-extra-scope=profile
```
### CA Certificate
You can use your self-signed certificate for the provider.
```yaml
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
- --certificate-authority-data=LS0t...
```
### HTTP Proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
### Authentication flows
#### 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
```
#### 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:
@@ -111,23 +275,34 @@ users:
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: kubectl
command: docker
args:
- oidc-login
- run
- --rm
- -v
- /tmp/.token-cache:/.token-cache
- -p
- 8000:8000
- quay.io/int128/kubelogin
- get-token
- -v1
- --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
```
You can verify kubelogin works with your provider using [acceptance test](acceptance_test).
Known limitations:
- It cannot open the browser automatically.
- The container port and listen port must be equal for consistency of the redirect URI.
## Docs
## Related works
- [Setup guide](docs/setup.md)
- [Usage and options](docs/usage.md)
- [Standalone mode](docs/standalone-mode.md)
- [System test](system_test)
- [Acceptance_test for identity providers](acceptance_test)
### Kubernetes Dashboard
You can access the Kubernetes Dashboard using kubelogin and [kauthproxy](https://github.com/int128/kauthproxy).
## Contributions
@@ -135,5 +310,17 @@ You can verify kubelogin works with your provider using [acceptance test](accept
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.
This software is developed with [GoLand](https://www.jetbrains.com/go/) licensed for open source development.
Special thanks for the support.
### 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 acceptance test](acceptance_test).

View File

@@ -1,31 +1,100 @@
CLUSTER_NAME := kubelogin-acceptance-test
OUTPUT_DIR := $(CURDIR)/output
PATH := $(PATH):$(OUTPUT_DIR)/bin
export PATH
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
# run the login script instead of opening chrome
BROWSER := $(OUTPUT_DIR)/bin/chromelogin
export BROWSER
.PHONY: test
test: build
# see the setup instruction
kubectl oidc-login setup \
--oidc-issuer-url=https://dex-server:10443/dex \
--oidc-client-id=YOUR_CLIENT_ID \
--oidc-client-secret=YOUR_CLIENT_SECRET \
--oidc-extra-scope=email \
--certificate-authority=$(OUTPUT_DIR)/ca.crt
# set up the kubeconfig
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=$(CURDIR)/../kubelogin \
--exec-command=kubectl \
--exec-arg=oidc-login \
--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
--exec-arg=--oidc-issuer-url=https://dex-server:10443/dex \
--exec-arg=--oidc-client-id=YOUR_CLIENT_ID \
--exec-arg=--oidc-client-secret=YOUR_CLIENT_SECRET \
--exec-arg=--oidc-extra-scope=email \
--exec-arg=--certificate-authority=$(OUTPUT_DIR)/ca.crt
# make sure we can access the cluster
kubectl --user=oidc cluster-info
# switch the current context
kubectl config set-context --current --user=oidc
# make sure we can access the cluster
kubectl cluster-info
.PHONY: setup
setup: build dex cluster setup-chrome
.PHONY: setup-chrome
setup-chrome: $(OUTPUT_DIR)/ca.crt
# add the dex server certificate to the trust store
mkdir -p ~/.pki/nssdb
cd ~/.pki/nssdb && certutil -A -d sql:. -n dex -i $(OUTPUT_DIR)/ca.crt -t "TC,,"
# build binaries
.PHONY: build
build: $(OUTPUT_DIR)/bin/kubectl-oidc_login $(OUTPUT_DIR)/bin/chromelogin
$(OUTPUT_DIR)/bin/kubectl-oidc_login:
go build -o $@ ..
$(OUTPUT_DIR)/bin/chromelogin: chromelogin/main.go
go build -o $@ ./chromelogin
# create a Dex server
.PHONY: dex
dex: $(OUTPUT_DIR)/server.crt $(OUTPUT_DIR)/server.key
docker create --name dex-server -p 10443:10443 --network kind quay.io/dexidp/dex:v2.21.0 serve /dex.yaml
docker cp $(OUTPUT_DIR)/server.crt dex-server:/
docker cp $(OUTPUT_DIR)/server.key dex-server:/
docker cp dex.yaml dex-server:/
docker start dex-server
docker logs dex-server
$(OUTPUT_DIR)/ca.key:
mkdir -p $(OUTPUT_DIR)
openssl genrsa -out $@ 2048
$(OUTPUT_DIR)/ca.csr: $(OUTPUT_DIR)/ca.key
openssl req -new -key $(OUTPUT_DIR)/ca.key -out $@ -subj "/CN=dex-ca" -config openssl.cnf
$(OUTPUT_DIR)/ca.crt: $(OUTPUT_DIR)/ca.key $(OUTPUT_DIR)/ca.csr
openssl x509 -req -in $(OUTPUT_DIR)/ca.csr -signkey $(OUTPUT_DIR)/ca.key -out $@ -days 10
$(OUTPUT_DIR)/server.key:
mkdir -p $(OUTPUT_DIR)
openssl genrsa -out $@ 2048
$(OUTPUT_DIR)/server.csr: openssl.cnf $(OUTPUT_DIR)/server.key
openssl req -new -key $(OUTPUT_DIR)/server.key -out $@ -subj "/CN=dex-server" -config openssl.cnf
$(OUTPUT_DIR)/server.crt: openssl.cnf $(OUTPUT_DIR)/server.csr $(OUTPUT_DIR)/ca.crt $(OUTPUT_DIR)/ca.key
openssl x509 -req -in $(OUTPUT_DIR)/server.csr -CA $(OUTPUT_DIR)/ca.crt -CAkey $(OUTPUT_DIR)/ca.key -CAcreateserial -out $@ -sha256 -days 10 -extensions v3_req -extfile openssl.cnf
# create a Kubernetes cluster
.PHONY: cluster
cluster: dex create-cluster
# add the Dex container IP to /etc/hosts of kube-apiserver
docker inspect -f '{{.NetworkSettings.IPAddress}}' dex-server | sed -e 's,$$, dex-server,' | \
kubectl -n kube-system exec -i kube-apiserver-$(CLUSTER_NAME)-control-plane -- tee -a /etc/hosts
# wait for kube-apiserver oidc initialization
# (oidc authenticator will retry oidc discovery every 10s)
sleep 10
.PHONY: create-cluster
create-cluster: $(OUTPUT_DIR)/ca.crt
cp $(OUTPUT_DIR)/ca.crt /tmp/kubelogin-acceptance-test-dex-ca.crt
kind create cluster --name $(CLUSTER_NAME) --config cluster.yaml
kubectl create clusterrole cluster-readonly --verb=get,watch,list --resource='*.*'
kubectl create clusterrolebinding cluster-readonly --clusterrole=cluster-readonly --user=admin@example.com
# clean up the resources
.PHONY: clean
@@ -34,9 +103,7 @@ clean:
.PHONY: delete-cluster
delete-cluster:
kind delete cluster --name $(CLUSTER_NAME)
.PHONY: check
check:
docker version
kind version
kubectl version --client
.PHONY: delete-dex
delete-dex:
docker stop dex-server
docker rm dex-server

View File

@@ -1,75 +1,109 @@
# kubelogin/acceptance_test
This is a manual test for verifying Kubernetes OIDC authentication with your OIDC provider.
This is an acceptance test for walkthrough of the OIDC initial setup and plugin behavior using a real Kubernetes cluster and OpenID Connect provider, running on [GitHub Actions](https://github.com/int128/kubelogin/actions?query=workflow%3Aacceptance-test).
It is intended to verify the following points:
- User can set up Kubernetes OIDC authentication and this plugin.
- User can access a cluster after login.
It performs the test using the following components:
- Kubernetes cluster (Kind)
- OIDC provider (Dex)
- Browser (Chrome)
- kubectl command
## Purpose
## How it works
This test checks the following points:
Let's take a look at the diagram.
1. You can set up your OIDC provider using [setup guide](../docs/setup.md).
1. The plugin works with your OIDC provider.
![diagram](../docs/acceptance-test-diagram.svg)
It prepares the following resources:
1. Generate a pair of CA certificate and TLS server certificate for Dex.
1. Run Dex on a container.
1. Create a Kubernetes cluster using Kind.
1. Mutate `/etc/hosts` of the CI machine to access Dex.
1. Mutate `/etc/hosts` of the kube-apiserver pod to access Dex.
It performs the test by the following steps:
1. Run kubectl.
1. kubectl automatically runs kubelogin.
1. kubelogin automatically runs [chromelogin](chromelogin).
1. chromelogin opens the browser, navigates to `http://localhost:8000` and enter the username and password.
1. kubelogin gets an authorization code from the browser.
1. kubelogin gets a token.
1. kubectl accesses an API with the token.
1. kube-apiserver verifies the token by Dex.
1. Check if kubectl exited with code 0.
## Getting Started
## Run locally
### 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:
You need to set up the following components:
- Docker
- Kind
- kubectl
- Chrome or Chromium
You can check if the tools are available.
You need to add the following line to `/etc/hosts` so that the browser can access the Dex.
```sh
make check
```
127.0.0.1 dex-server
```
### 1. Create a cluster
Run the test.
Create a cluster.
For example, you can create a cluster with Google account authentication.
```shell script
# run the test
make
```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
# clean up
make delete-cluster
make clean
make delete-dex
```
## Technical consideration
### Network and DNS
Consider the following issues:
- kube-apiserver runs on the host network of the kind container.
- kube-apiserver cannot resolve a service name by kube-dns.
- kube-apiserver cannot access a cluster IP.
- kube-apiserver can access another container via the Docker network.
- Chrome requires exactly match of domain name between Dex URL and a server certificate.
Consequently,
- kube-apiserver accesses Dex by resolving `/etc/hosts` and via the Docker network.
- kubelogin and Chrome accesses Dex by resolving `/etc/hosts` and via the Docker network.
### TLS server certificate
Consider the following issues:
- kube-apiserver requires `--oidc-issuer` is HTTPS URL.
- kube-apiserver requires a CA certificate at startup, if `--oidc-ca-file` is given.
- kube-apiserver mounts `/usr/local/share/ca-certificates` from the kind container.
- It is possible to mount a file from the CI machine.
- It is not possible to issue a certificate using Let's Encrypt in runtime.
- Chrome requires a valid certificate in `~/.pki/nssdb`.
As a result,
- kube-apiserver uses the CA certificate of `/usr/local/share/ca-certificates/dex-ca.crt`. See the `extraMounts` section of [`cluster.yaml`](cluster.yaml).
- kubelogin uses the CA certificate in `output/ca.crt`.
- Chrome uses the CA certificate in `~/.pki/nssdb`.
### Test environment
- Set the issuer URL to kubectl. See [`kubeconfig_oidc.yaml`](kubeconfig_oidc.yaml).
- Set the issuer URL to kube-apiserver. See [`cluster.yaml`](cluster.yaml).
- Set `BROWSER` environment variable to run [`chromelogin`](chromelogin) by `xdg-open`.

View File

@@ -1,5 +1,6 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
# https://github.com/dexidp/dex/blob/master/Documentation/kubernetes.md
kubeadmConfigPatches:
- |
apiVersion: kubeadm.k8s.io/v1beta2
@@ -8,6 +9,12 @@ kubeadmConfigPatches:
name: config
apiServer:
extraArgs:
oidc-issuer-url: OIDC_ISSUER_URL
oidc-client-id: OIDC_CLIENT_ID
oidc-issuer-url: https://dex-server:10443/dex
oidc-client-id: YOUR_CLIENT_ID
oidc-username-claim: email
oidc-ca-file: /usr/local/share/ca-certificates/dex-ca.crt
nodes:
- role: control-plane
extraMounts:
- hostPath: /tmp/kubelogin-acceptance-test-dex-ca.crt
containerPath: /usr/local/share/ca-certificates/dex-ca.crt

13
dist/Dockerfile vendored Normal file
View File

@@ -0,0 +1,13 @@
FROM alpine:3.11
ARG KUBELOGIN_VERSION="{{ env "VERSION" }}"
ARG KUBELOGIN_SHA256="{{ sha256 .linux_amd64_archive }}"
# Download the release and test the checksum
RUN wget -O /kubelogin.zip "https://github.com/int128/kubelogin/releases/download/$KUBELOGIN_VERSION/kubelogin_linux_amd64.zip" && \
echo "$KUBELOGIN_SHA256 /kubelogin.zip" | sha256sum -c - && \
unzip /kubelogin.zip && \
rm /kubelogin.zip
USER daemon
ENTRYPOINT ["/kubelogin"]

27
dist/kubelogin.rb vendored Normal file
View File

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

88
dist/oidc-login.yaml vendored Normal file
View File

@@ -0,0 +1,88 @@
apiVersion: krew.googlecontainertools.github.com/v1alpha2
kind: Plugin
metadata:
name: oidc-login
spec:
homepage: https://github.com/int128/kubelogin
shortDescription: Log in to the OpenID Connect provider
description: |
This is a kubectl plugin for Kubernetes OpenID Connect (OIDC) authentication.
## Credential plugin mode
kubectl executes oidc-login before calling the Kubernetes APIs.
oidc-login automatically opens the browser and you can log in to the provider.
After authentication, kubectl gets the token from oidc-login and you can access the cluster.
See https://github.com/int128/kubelogin#credential-plugin-mode for more.
## Standalone mode
Run `kubectl oidc-login`.
It automatically opens the browser and you can log in to the provider.
After authentication, it writes the token to the kubeconfig and you can access the cluster.
See https://github.com/int128/kubelogin#standalone-mode for more.
caveats: |
You need to setup the OIDC provider, Kubernetes API server, role binding and kubeconfig.
See https://github.com/int128/kubelogin for more.
version: {{ env "VERSION" }}
platforms:
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_amd64.zip
sha256: "{{ sha256 .linux_amd64_archive }}"
bin: kubelogin
files:
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_darwin_amd64.zip
sha256: "{{ sha256 .darwin_amd64_archive }}"
bin: kubelogin
files:
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: darwin
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_windows_amd64.zip
sha256: "{{ sha256 .windows_amd64_archive }}"
bin: kubelogin.exe
files:
- from: kubelogin.exe
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: windows
arch: amd64
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm.zip
sha256: "{{ sha256 .linux_arm_archive }}"
bin: kubelogin
files:
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
arch: arm
- uri: https://github.com/int128/kubelogin/releases/download/{{ env "VERSION" }}/kubelogin_linux_arm64.zip
sha256: "{{ sha256 .linux_arm64_archive }}"
bin: kubelogin
files:
- from: kubelogin
to: .
- from: LICENSE
to: .
selector:
matchLabels:
os: linux
arch: arm64

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -126,28 +126,6 @@ Variable | Value
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).
### Ping Identity
Login with an account that has permissions to create applications.
Create an OIDC application with the following configuration:
- Redirect URIs:
- `http://localhost:8000`
- `http://localhost:18000` (used if the port 8000 is already in use)
- Grant type: Authorization Code
- PKCE Enforcement: Required
Leverage the following variables in the next steps.
Variable | Value
------------------------|------
`ISSUER_URL` | `https://auth.pingone.com/<PingOne Tenant Id>/as`
`YOUR_CLIENT_ID` | random string
`YOUR_CLIENT_SECRET` is not required for this configuration.
## 2. Verify authentication

View File

@@ -1,12 +1,9 @@
# Standalone mode
Kubelogin supports the standalone mode as well.
It writes the token to the kubeconfig (typically `~/.kube/config`) after authentication.
You can run kubelogin as a standalone command.
In this mode, you need to manually run the command before running kubectl.
## Getting started
Configure your kubeconfig like:
Configure the kubeconfig like:
```yaml
- name: keycloak
@@ -34,7 +31,7 @@ It automatically opens the browser and you can log in to the provider.
After authentication, kubelogin writes the ID token and refresh token to the kubeconfig.
```console
```
% kubelogin
Open http://localhost:8000 for authentication
You got a valid token until 2019-05-18 10:28:51 +0900 JST
@@ -43,7 +40,7 @@ Updated ~/.kubeconfig
Now you can access the cluster.
```console
```
% kubectl get pods
NAME READY STATUS RESTARTS AGE
echoserver-86c78fdccd-nzmd5 1/1 Running 0 26d
@@ -67,7 +64,7 @@ users:
If the ID token is valid, kubelogin does nothing.
```console
```
% kubelogin
You already have a valid token until 2019-05-18 10:28:51 +0900 JST
```
@@ -78,6 +75,61 @@ If the refresh token has expired, kubelogin will proceed the authentication.
## Usage
Kubelogin supports the following options:
```
% kubectl oidc-login -h
Login to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
Usage:
main [flags]
main [command]
Available Commands:
get-token Run as a kubectl credential plugin
help Help about any command
setup Show the setup instruction
version Print the version information
Flags:
--kubeconfig string Path to the kubeconfig file
--context string The name of the kubeconfig context to use
--user string The name of the kubeconfig user to use. Prior to --context
--certificate-authority string Path to a cert file for the certificate authority
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--grant-type string The authorization grant type to use. One of (auto|authcode|authcode-keyboard|password) (default "auto")
--listen-address strings Address to bind to the local server. If multiple addresses are given, it will try binding in order (default [127.0.0.1:8000,127.0.0.1:18000])
--listen-port ints (Deprecated: use --listen-address)
--skip-open-browser If true, it does not open the browser on authentication
--oidc-redirect-url-hostname string Hostname of the redirect URL (default "localhost")
--oidc-auth-request-extra-params stringToString Extra query parameters to send with an authentication request (default [])
--username string If set, perform the resource owner password credentials grant
--password string If set, use the password instead of asking it
--add_dir_header If true, adds the file directory to the header
--alsologtostderr log to standard error as well as files
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
-h, --help help for kubelogin
--version version for kubelogin
```
### Kubeconfig
You can set path to the kubeconfig file by the option or the environment variable just like kubectl.
It defaults to `~/.kube/config`.
@@ -105,4 +157,26 @@ Key | Direction | Value
`id-token` | Write | ID token got from the provider.
`refresh-token` | Write | Refresh token got from the provider.
See also [usage.md](usage.md).
### Extra scopes
You can set the extra scopes to request to the provider by `extra-scopes` in the kubeconfig.
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=email
```
Currently kubectl does not accept multiple scopes, so you need to edit the kubeconfig as like:
```sh
kubectl config set-credentials keycloak --auth-provider-arg extra-scopes=SCOPES
sed -i '' -e s/SCOPES/email,profile/ $KUBECONFIG
```
### CA Certificates
You can use your self-signed certificates for the provider.
```sh
kubectl config set-credentials keycloak \
--auth-provider-arg idp-certificate-authority=$HOME/.kube/keycloak-ca.pem
```

View File

@@ -1,252 +0,0 @@
# Usage
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
--oidc-use-pkce Force PKCE usage
--token-cache-dir string Path to a directory for token cache (default "~/.kube/cache/oidc-login")
--certificate-authority stringArray Path to a cert file for the certificate authority
--certificate-authority-data stringArray 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
--tls-renegotiation-once If set, allow a remote server to request renegotiation once per connection
--tls-renegotiation-freely If set, allow a remote server to repeatedly request renegotiation
--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
--browser-command string [authcode] Command to open the browser
--authentication-timeout-sec int [authcode] Timeout of authentication in seconds (default 180)
--local-server-cert string [authcode] Certificate path for the local server
--local-server-key string [authcode] Certificate key path for the local server
--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 of the log messages
--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)
--one_output If true, only write logs to their native severity level (vs also writing to each lower severity level)
--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
```
## Options
### Authentication timeout
By default, you need to log in to your provider in the browser within 3 minutes.
This prevents a process from remaining forever.
You can change the timeout by the following flag:
```yaml
- --authentication-timeout-sec=60
```
For now this timeout works only for the authorization code flow.
### Extra scopes
You can set the extra scopes to request to the provider by `--oidc-extra-scope`.
```yaml
- --oidc-extra-scope=email
- --oidc-extra-scope=profile
```
### CA certificate
You can use your self-signed certificate for the provider.
```yaml
- --certificate-authority=/home/user/.kube/keycloak-ca.pem
- --certificate-authority-data=LS0t...
```
### HTTP proxy
You can set the following environment variables if you are behind a proxy: `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`.
See also [net/http#ProxyFromEnvironment](https://golang.org/pkg/net/http/#ProxyFromEnvironment).
### Home directory expansion
If a value in the following options begins with a tilde character `~`, it is expanded to the home directory.
- `--certificate-authority`
- `--local-server-cert`
- `--local-server-key`
- `--token-cache-dir`
## Authentication flows
Kubelogin support the following flows:
- Authorization code flow
- Authorization code flow with a keyboard
- Resource owner password credentials grant flow
### 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 specify a certificate for the local webserver if HTTPS is required by your identity provider.
```yaml
- --local-server-cert=localhost.crt
- --local-server-key=localhost.key
```
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
```
You can skip opening the browser if you encounter some environment problem.
```yaml
- --skip-open-browser
```
For Linux users, you change the default browser by `BROWSER` environment variable.
### Authorization code flow with a keyboard
If you cannot access the browser, instead use the authorization code flow with a keyboard.
```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:
```
## Run in Docker
You can run [the Docker image](https://ghcr.io/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
- ghcr.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.

39
go.mod
View File

@@ -1,25 +1,26 @@
module github.com/int128/kubelogin
go 1.16
go 1.12
require (
github.com/alexflint/go-filemutex v1.2.0
github.com/chromedp/chromedp v0.8.5
github.com/coreos/go-oidc/v3 v3.2.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/go-cmp v0.5.9
github.com/google/wire v0.5.0
github.com/int128/oauth2cli v1.14.0
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/spf13/cobra v1.5.0
github.com/chromedp/chromedp v0.5.3
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/golang/mock v1.4.3
github.com/google/go-cmp v0.4.1
github.com/google/wire v0.4.0
github.com/int128/oauth2cli v1.11.0
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/spf13/cobra v0.0.7
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.0
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
golang.org/x/sync v0.0.0-20220907140024-f12130a52804
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
gopkg.in/yaml.v2 v2.4.0
k8s.io/apimachinery v0.24.4
k8s.io/client-go v0.24.4
k8s.io/klog/v2 v2.70.1
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
gopkg.in/yaml.v2 v2.3.0
k8s.io/apimachinery v0.18.3
k8s.io/client-go v0.18.3
k8s.io/klog v1.0.0
)

922
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"testing"
"time"
@@ -13,8 +14,8 @@ import (
"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/infrastructure/browser"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -29,9 +30,17 @@ import (
// 4. Verify the output.
//
func TestCredentialPlugin(t *testing.T) {
timeout := 10 * time.Second
timeout := 3 * time.Second
now := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
tokenCacheDir := t.TempDir()
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
@@ -43,11 +52,6 @@ func TestCredentialPlugin(t *testing.T) {
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()
@@ -62,11 +66,12 @@ func TestCredentialPlugin(t *testing.T) {
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),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now,
stdout: &stdout,
args: tc.args,
@@ -89,6 +94,7 @@ func TestCredentialPlugin(t *testing.T) {
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
var stdout bytes.Buffer
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
@@ -109,6 +115,7 @@ func TestCredentialPlugin(t *testing.T) {
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{
@@ -125,7 +132,7 @@ func TestCredentialPlugin(t *testing.T) {
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now,
stdout: &stdout,
args: tc.args,
@@ -161,7 +168,7 @@ func TestCredentialPlugin(t *testing.T) {
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now.Add(2 * time.Hour),
stdout: &stdout,
args: tc.args,
@@ -183,7 +190,7 @@ func TestCredentialPlugin(t *testing.T) {
runGetToken(t, ctx, getTokenConfig{
tokenCacheDir: tokenCacheDir,
issuerURL: sv.IssuerURL(),
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now.Add(4 * time.Hour),
stdout: &stdout,
args: tc.args,
@@ -209,11 +216,12 @@ func TestCredentialPlugin(t *testing.T) {
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"}),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
stdout: &stdout,
})
@@ -233,11 +241,12 @@ func TestCredentialPlugin(t *testing.T) {
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"}),
httpDriver: httpdriver.New(ctx, t, keypair.Server.TLSConfig),
now: now,
stdout: &stdout,
args: []string{"--certificate-authority-data", keypair.Server.CACertBase64},
@@ -258,11 +267,12 @@ func TestCredentialPlugin(t *testing.T) {
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"}),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
stdout: &stdout,
args: []string{
@@ -273,31 +283,6 @@ func TestCredentialPlugin(t *testing.T) {
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),
},
})
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)
@@ -311,11 +296,12 @@ func TestCredentialPlugin(t *testing.T) {
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"}),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
stdout: &stdout,
args: []string{"--oidc-redirect-url-hostname", "127.0.0.1"},
@@ -323,37 +309,6 @@ func TestCredentialPlugin(t *testing.T) {
assertCredentialPluginStdout(t, &stdout, sv.LastTokenResponse().IDToken, now.Add(time.Hour))
})
t.Run("RedirectURLHTTPS", 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: "https://localhost:",
},
Response: oidcserver.Response{
IDTokenExpiry: now.Add(time.Hour),
},
})
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{
"--local-server-cert", keypair.Server.CertPath,
"--local-server-key", keypair.Server.KeyPath,
},
})
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)
@@ -371,11 +326,12 @@ func TestCredentialPlugin(t *testing.T) {
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"}),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
stdout: &stdout,
args: []string{

View File

@@ -4,20 +4,13 @@ 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}
func New(ctx context.Context, t *testing.T, tlsConfig *tls.Config) *client {
return &client{ctx, t, tlsConfig}
}
// Zero returns a client which call is not expected.
@@ -26,13 +19,13 @@ func Zero(t *testing.T) *zeroClient {
}
type client struct {
ctx context.Context
t *testing.T
o Option
ctx context.Context
t *testing.T
tlsConfig *tls.Config
}
func (c *client) Open(url string) error {
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.o.TLSConfig}}
client := http.Client{Transport: &http.Transport{TLSClientConfig: c.tlsConfig}}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
c.t.Errorf("could not create a request: %s", err)
@@ -48,22 +41,9 @@ func (c *client) Open(url string) error {
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
}
func (c *client) OpenCommand(_ context.Context, url, _ string) error {
return c.Open(url)
}
type zeroClient struct {
t *testing.T
}
@@ -72,7 +52,3 @@ func (c *zeroClient) Open(url string) error {
c.t.Errorf("unexpected function call Open(%s)", url)
return nil
}
func (c *zeroClient) OpenCommand(_ context.Context, url, _ string) error {
return c.Open(url)
}

View File

@@ -2,8 +2,8 @@ package kubeconfig
import (
"html/template"
"io/ioutil"
"os"
"path/filepath"
"testing"
"gopkg.in/yaml.v2"
@@ -22,8 +22,7 @@ type Values struct {
// Create creates a kubeconfig file and returns path to it.
func Create(t *testing.T, v *Values) string {
t.Helper()
name := filepath.Join(t.TempDir(), "kubeconfig")
f, err := os.Create(name)
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatal(err)
}
@@ -35,7 +34,7 @@ func Create(t *testing.T, v *Values) string {
if err := tpl.Execute(f, v); err != nil {
t.Fatal(err)
}
return name
return f.Name()
}
type AuthProviderConfig struct {

View File

@@ -3,10 +3,11 @@ package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"golang.org/x/xerrors"
)
func New(t *testing.T, provider Provider) *Handler {
@@ -28,7 +29,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.t.Logf("%d %s %s", wr.statusCode, r.Method, r.RequestURI)
return
}
if errResp := new(ErrorResponse); errors.As(err, &errResp) {
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)
@@ -61,14 +62,14 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
w.Header().Add("Content-Type", "application/json")
e := json.NewEncoder(w)
if err := e.Encode(discoveryResponse); err != nil {
return fmt.Errorf("could not render json: %w", err)
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 fmt.Errorf("could not render json: %w", err)
return xerrors.Errorf("could not render json: %w", err)
}
case m == "GET" && p == "/auth":
q := r.URL.Query()
@@ -83,13 +84,13 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
RawQuery: q,
})
if err != nil {
return fmt.Errorf("authentication error: %w", err)
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 fmt.Errorf("could not parse the form: %w", err)
return xerrors.Errorf("could not parse the form: %w", err)
}
grantType := r.Form.Get("grant_type")
switch grantType {
@@ -99,12 +100,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
CodeVerifier: r.Form.Get("code_verifier"),
})
if err != nil {
return fmt.Errorf("token request error: %w", err)
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 fmt.Errorf("could not render json: %w", err)
return xerrors.Errorf("could not render json: %w", err)
}
case "password":
// 4.3. Resource Owner Password Credentials Grant
@@ -112,12 +113,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
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 fmt.Errorf("authentication error: %w", err)
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 fmt.Errorf("could not render json: %w", err)
return xerrors.Errorf("could not render json: %w", err)
}
case "refresh_token":
// 12.1. Refresh Request
@@ -125,12 +126,12 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
refreshToken := r.Form.Get("refresh_token")
tokenResponse, err := h.provider.Refresh(refreshToken)
if err != nil {
return fmt.Errorf("token refresh error: %w", err)
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 fmt.Errorf("could not render json: %w", err)
return xerrors.Errorf("could not render json: %w", err)
}
default:
// 5.2. Error Response

View File

@@ -10,14 +10,28 @@ import (
"github.com/int128/kubelogin/integration_test/keypair"
)
func Start(t *testing.T, h http.Handler, k keypair.KeyPair) string {
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 {
func startNoTLS(t *testing.T, h http.Handler) (string, *shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "http://localhost:" + port
@@ -30,15 +44,10 @@ func startNoTLS(t *testing.T, h http.Handler) string {
t.Error(err)
}
}()
t.Cleanup(func() {
if err := s.Shutdown(context.TODO()); err != nil {
t.Errorf("could not shutdown the server: %s", err)
}
})
return url
return url, &shutdowner{s}
}
func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) string {
func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) (string, *shutdowner) {
t.Helper()
l, port := newLocalhostListener(t)
url := "https://localhost:" + port
@@ -51,12 +60,7 @@ func startTLS(t *testing.T, h http.Handler, k keypair.KeyPair) string {
t.Error(err)
}
}()
t.Cleanup(func() {
if err := s.Shutdown(context.TODO()); err != nil {
t.Errorf("could not shutdown the server: %s", err)
}
})
return url
return url, &shutdowner{s}
}
func newLocalhostListener(t *testing.T) (net.Listener, string) {

View File

@@ -4,7 +4,6 @@ package oidcserver
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"math/big"
"strings"
"testing"
@@ -14,9 +13,11 @@ import (
"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
@@ -50,12 +51,13 @@ type Config struct {
// 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 = http.Start(t, handler.New(t, &sv), k)
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
@@ -131,7 +133,7 @@ func (sv *server) AuthenticateCode(req handler.AuthenticationRequest) (code stri
func (sv *server) Exchange(req handler.TokenRequest) (*handler.TokenResponse, error) {
if req.Code != "YOUR_AUTH_CODE" {
return nil, fmt.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.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

View File

@@ -10,8 +10,8 @@ import (
"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/infrastructure/browser"
"github.com/int128/kubelogin/pkg/testing/clock"
"github.com/int128/kubelogin/pkg/testing/logger"
)
@@ -36,11 +36,6 @@ func TestStandalone(t *testing.T) {
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()
@@ -55,14 +50,16 @@ func TestStandalone(t *testing.T) {
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),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -86,10 +83,12 @@ func TestStandalone(t *testing.T) {
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,
@@ -111,10 +110,12 @@ func TestStandalone(t *testing.T) {
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{
@@ -130,7 +131,7 @@ func TestStandalone(t *testing.T) {
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -166,7 +167,7 @@ func TestStandalone(t *testing.T) {
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now.Add(2 * time.Hour),
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -188,7 +189,7 @@ func TestStandalone(t *testing.T) {
runStandalone(t, ctx, standaloneConfig{
issuerURL: sv.IssuerURL(),
kubeConfigFilename: kubeConfigFilename,
httpDriver: httpdriver.New(ctx, t, httpDriverOption),
httpDriver: httpdriver.New(ctx, t, tc.keyPair.TLSConfig),
now: now.Add(4 * time.Hour),
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -213,14 +214,16 @@ func TestStandalone(t *testing.T) {
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}),
httpDriver: httpdriver.New(ctx, t, keypair.Server.TLSConfig),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -241,13 +244,16 @@ func TestStandalone(t *testing.T) {
IDTokenExpiry: now.Add(time.Hour),
},
})
defer sv.Shutdown(t, ctx)
kubeConfigFilename := kubeconfig.Create(t, &kubeconfig.Values{
Issuer: sv.IssuerURL(),
})
t.Setenv("KUBECONFIG", kubeConfigFilename+string(os.PathListSeparator)+"kubeconfig/testdata/dummy.yaml")
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{}),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -269,14 +275,16 @@ func TestStandalone(t *testing.T) {
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{}),
httpDriver: httpdriver.New(ctx, t, nil),
now: now,
})
kubeconfig.Verify(t, kubeConfigFilename, kubeconfig.AuthProviderConfig{
@@ -305,3 +313,17 @@ func runStandalone(t *testing.T, ctx context.Context, cfg standaloneConfig) {
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)
}
}

View File

@@ -1,14 +1,14 @@
package browser
import (
"context"
"os"
"os/exec"
"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.
@@ -23,7 +23,6 @@ var Set = wire.NewSet(
type Interface interface {
Open(url string) error
OpenCommand(ctx context.Context, url, command string) error
}
type Browser struct{}
@@ -32,11 +31,3 @@ type Browser struct{}
func (*Browser) Open(url string) error {
return browser.OpenURL(url)
}
// OpenCommand opens the browser using the command.
func (*Browser) OpenCommand(ctx context.Context, url, command string) error {
c := exec.CommandContext(ctx, command, url)
c.Stdout = os.Stderr // see above
c.Stderr = os.Stderr
return c.Run()
}

View File

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

View File

@@ -0,0 +1,71 @@
// Package certpool provides loading certificates from files or base64 encoded string.
package certpool
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"io/ioutil"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_certpool/mock_certpool.go github.com/int128/kubelogin/pkg/adaptors/certpool Interface
// Set provides an implementation and interface.
var Set = wire.NewSet(
wire.Value(NewFunc(New)),
wire.Struct(new(CertPool), "*"),
wire.Bind(new(Interface), new(*CertPool)),
)
type NewFunc func() Interface
// New returns an instance which implements the Interface.
func New() Interface {
return &CertPool{pool: x509.NewCertPool()}
}
type Interface interface {
AddFile(filename string) error
AddBase64Encoded(s string) error
SetRootCAs(cfg *tls.Config)
}
// CertPool represents a pool of certificates.
type CertPool struct {
pool *x509.CertPool
}
// SetRootCAs sets cfg.RootCAs if it has any certificate.
// Otherwise do nothing.
func (p *CertPool) SetRootCAs(cfg *tls.Config) {
if len(p.pool.Subjects()) > 0 {
cfg.RootCAs = p.pool
}
}
// AddFile loads the certificate from the file.
func (p *CertPool) AddFile(filename string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return xerrors.Errorf("could not read %s: %w", filename, err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate from %s", filename)
}
return nil
}
// AddBase64Encoded loads the certificate from the base64 encoded string.
func (p *CertPool) AddBase64Encoded(s string) error {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return xerrors.Errorf("could not decode base64: %w", err)
}
if !p.pool.AppendCertsFromPEM(b) {
return xerrors.Errorf("could not append certificate")
}
return nil
}

View File

@@ -0,0 +1,58 @@
package certpool
import (
"crypto/tls"
"io/ioutil"
"testing"
)
func TestCertPool_AddFile(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
p := New()
if err := p.AddFile("testdata/ca1.crt"); err != nil {
t.Errorf("AddFile error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
})
t.Run("Invalid", func(t *testing.T) {
p := New()
err := p.AddFile("testdata/Makefile")
if err == nil {
t.Errorf("AddFile wants an error but was nil")
}
})
}
func TestCertPool_AddBase64Encoded(t *testing.T) {
p := New()
if err := p.AddBase64Encoded(readFile(t, "testdata/ca2.crt.base64")); err != nil {
t.Errorf("AddBase64Encoded error: %s", err)
}
var cfg tls.Config
p.SetRootCAs(&cfg)
if n := len(cfg.RootCAs.Subjects()); n != 1 {
t.Errorf("n wants 1 but was %d", n)
}
}
func TestCertPool_SetRootCAs(t *testing.T) {
p := New()
var cfg tls.Config
p.SetRootCAs(&cfg)
if cfg.RootCAs != nil {
t.Errorf("cfg.RootCAs wants nil but was %+v", cfg.RootCAs)
}
}
func readFile(t *testing.T, filename string) string {
t.Helper()
b, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatalf("ReadFile error: %s", err)
}
return string(b)
}

View File

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

View File

@@ -2,12 +2,12 @@ package cmd
import (
"context"
"path/filepath"
"runtime"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"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.
@@ -24,9 +24,7 @@ type Interface interface {
}
var defaultListenAddress = []string{"127.0.0.1:8000", "127.0.0.1:18000"}
var defaultTokenCacheDir = filepath.Join("~", ".kube", "cache", "oidc-login")
const defaultAuthenticationTimeoutSec = 180
var defaultTokenCacheDir = homedir.HomeDir() + "/.kube/cache/oidc-login"
// Cmd provides interaction with command line interface (CLI).
type Cmd struct {

View File

@@ -0,0 +1,374 @@
package cmd
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin/mock_credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/int128/kubelogin/pkg/usecases/standalone/mock_standalone"
)
func TestCmd_Run(t *testing.T) {
const executable = "kubelogin"
const version = "HEAD"
t.Run("root", func(t *testing.T) {
tests := map[string]struct {
args []string
in standalone.Input
}{
"Defaults": {
args: []string{executable},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: defaultListenAddress,
RedirectURLHostname: "localhost",
},
},
},
},
"when --listen-port is set, it should convert the port to address": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
},
"when --listen-port is set, it should ignore --listen-address flags": {
args: []string{
executable,
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:30080",
"--listen-address", "127.0.0.1:40080",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"--certificate-authority", "/path/to/cacert",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
CACertFilename: "/path/to/cacert",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
RedirectURLHostname: "localhost",
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"--grant-type", "authcode-keyboard",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{},
},
},
},
"GrantType=password": {
args: []string{executable,
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: standalone.Input{
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
mockStandalone := mock_standalone.NewMockInterface(ctrl)
mockStandalone.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Standalone: mockStandalone,
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cmd := Cmd{
Root: &Root{
Standalone: mock_standalone.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(context.TODO(), []string{executable, "some"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
t.Run("get-token", func(t *testing.T) {
tests := map[string]struct {
args []string
in credentialplugin.Input
}{
"Defaults": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
"--oidc-extra-scope", "email",
"--oidc-extra-scope", "profile",
"--certificate-authority", "/path/to/cacert",
"--certificate-authority-data", "BASE64ENCODED",
"--insecure-skip-tls-verify",
"-v1",
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=true",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
CACertFilename: "/path/to/cacert",
CACertData: "BASE64ENCODED",
SkipTLSVerify: true,
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeOption: &authentication.AuthCodeOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
RedirectURLHostname: "localhost",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
},
},
},
},
"GrantType=authcode-keyboard": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "authcode-keyboard",
"--oidc-auth-request-extra-params", "ttl=86400",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authentication.AuthCodeKeyboardOption{
AuthRequestExtraParams: map[string]string{"ttl": "86400"},
},
},
},
},
"GrantType=password": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
"GrantType=auto": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
in: credentialplugin.Input{
TokenCacheDir: defaultTokenCacheDir,
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
GrantOptionSet: authentication.GrantOptionSet{
ROPCOption: &authentication.ROPCOption{
Username: "USER",
Password: "PASS",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
getToken := mock_credentialplugin.NewMockInterface(ctrl)
getToken.EXPECT().
Do(ctx, c.in)
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: getToken,
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, c.args, version)
if exitCode != 0 {
t.Errorf("exitCode wants 0 but %d", exitCode)
}
})
}
t.Run("MissingMandatoryOptions", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
t.Run("TooManyArgs", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: mock_credentialplugin.NewMockInterface(ctrl),
Logger: logger.New(t),
},
Logger: logger.New(t),
}
exitCode := cmd.Run(ctx, []string{executable, "get-token", "foo"}, version)
if exitCode != 1 {
t.Errorf("exitCode wants 1 but %d", exitCode)
}
})
})
}

View File

@@ -1,14 +1,11 @@
package cmd
import (
"errors"
"fmt"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"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.
@@ -17,28 +14,24 @@ type getTokenOptions struct {
ClientID string
ClientSecret string
ExtraScopes []string
UsePKCE bool
CACertFilename string
CACertData string
SkipTLSVerify bool
TokenCacheDir string
tlsOptions tlsOptions
authenticationOptions authenticationOptions
}
func (o *getTokenOptions) addFlags(f *pflag.FlagSet) {
func (o *getTokenOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider (mandatory)")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider (mandatory)")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE usage")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for token cache")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
}
func (o *getTokenOptions) expandHomedir() error {
o.TokenCacheDir = expandHomedir(o.TokenCacheDir)
o.authenticationOptions.expandHomedir()
o.tlsOptions.expandHomedir()
return nil
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded data for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
f.StringVar(&o.TokenCacheDir, "token-cache-dir", defaultTokenCacheDir, "Path to a directory for caching tokens")
o.authenticationOptions.register(f)
}
type GetToken struct {
@@ -56,40 +49,35 @@ func (cmd *GetToken) New() *cobra.Command {
return err
}
if o.IssuerURL == "" {
return errors.New("--oidc-issuer-url is missing")
return xerrors.New("--oidc-issuer-url is missing")
}
if o.ClientID == "" {
return errors.New("--oidc-client-id is missing")
return xerrors.New("--oidc-client-id is missing")
}
return nil
},
RunE: func(c *cobra.Command, _ []string) error {
if err := o.expandHomedir(); err != nil {
return err
}
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return fmt.Errorf("get-token: %w", err)
return xerrors.Errorf("get-token: %w", err)
}
in := credentialplugin.Input{
Provider: oidc.Provider{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
UsePKCE: o.UsePKCE,
ExtraScopes: o.ExtraScopes,
},
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CACertFilename,
CACertData: o.CACertData,
SkipTLSVerify: o.SkipTLSVerify,
TokenCacheDir: o.TokenCacheDir,
GrantOptionSet: grantOptionSet,
}
if err := cmd.GetToken.Do(c.Context(), in); err != nil {
return fmt.Errorf("get-token: %w", err)
return xerrors.Errorf("get-token: %w", err)
}
return nil
},
}
c.Flags().SortFlags = false
o.addFlags(c.Flags())
o.register(c.Flags())
return c
}

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

@@ -0,0 +1,149 @@
package cmd
import (
"fmt"
"strings"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
const longDescription = `Login to the OpenID Connect provider.
You need to set up the OIDC provider, role binding, Kubernetes API server and kubeconfig.
Run the following command to show the setup instruction:
kubectl oidc-login setup
See https://github.com/int128/kubelogin for more.
`
// rootOptions represents the options for the root command.
type rootOptions struct {
Kubeconfig string
Context string
User string
CertificateAuthority string
SkipTLSVerify bool
authenticationOptions authenticationOptions
}
func (o *rootOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.Kubeconfig, "kubeconfig", "", "Path to the kubeconfig file")
f.StringVar(&o.Context, "context", "", "The name of the kubeconfig context to use")
f.StringVar(&o.User, "user", "", "The name of the kubeconfig user to use. Prior to --context")
f.StringVar(&o.CertificateAuthority, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
}
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
SkipOpenBrowser bool
RedirectURLHostname string
AuthRequestExtraParams map[string]string
Username string
Password string
}
// determineListenAddress returns the addresses from the flags.
// Note that --listen-address is always given due to the default value.
// If --listen-port is not set, it returns --listen-address.
// If --listen-port is set, it returns the strings of --listen-port.
func (o *authenticationOptions) determineListenAddress() []string {
if len(o.ListenPort) == 0 {
return o.ListenAddress
}
var a []string
for _, p := range o.ListenPort {
a = append(a, fmt.Sprintf("127.0.0.1:%d", p))
}
return a
}
var allGrantType = strings.Join([]string{
"auto",
"authcode",
"authcode-keyboard",
"password",
}, "|")
func (o *authenticationOptions) register(f *pflag.FlagSet) {
f.StringVar(&o.GrantType, "grant-type", "auto", fmt.Sprintf("The authorization grant type to use. One of (%s)", allGrantType))
f.StringSliceVar(&o.ListenAddress, "listen-address", defaultListenAddress, "Address to bind to the local server. If multiple addresses are given, it will try binding in order")
//TODO: remove the deprecated flag
f.IntSliceVar(&o.ListenPort, "listen-port", nil, "(Deprecated: use --listen-address)")
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "Hostname of the redirect URL")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "Extra query parameters to send with an authentication request")
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
}
func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSet, err error) {
switch {
case o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == ""):
s.AuthCodeOption = &authentication.AuthCodeOption{
BindAddress: o.determineListenAddress(),
SkipOpenBrowser: o.SkipOpenBrowser,
RedirectURLHostname: o.RedirectURLHostname,
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "authcode-keyboard":
s.AuthCodeKeyboardOption = &authentication.AuthCodeKeyboardOption{
AuthRequestExtraParams: o.AuthRequestExtraParams,
}
case o.GrantType == "password" || (o.GrantType == "auto" && o.Username != ""):
s.ROPCOption = &authentication.ROPCOption{
Username: o.Username,
Password: o.Password,
}
default:
err = xerrors.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}
type Root struct {
Standalone standalone.Interface
Logger logger.Interface
}
func (cmd *Root) New() *cobra.Command {
var o rootOptions
rootCmd := &cobra.Command{
Use: "kubelogin",
Short: "Login to the OpenID Connect provider",
Long: longDescription,
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return xerrors.Errorf("invalid option: %w", err)
}
in := standalone.Input{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
CACertFilename: o.CertificateAuthority,
SkipTLSVerify: o.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
}
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
return xerrors.Errorf("login: %w", err)
}
return nil
},
}
o.register(rootCmd.Flags())
cmd.Logger.AddFlags(rootCmd.PersistentFlags())
return rootCmd
}

View File

@@ -1,11 +1,10 @@
package cmd
import (
"fmt"
"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.
@@ -14,19 +13,22 @@ type setupOptions struct {
ClientID string
ClientSecret string
ExtraScopes []string
UsePKCE bool
tlsOptions tlsOptions
CACertFilename string
CACertData string
SkipTLSVerify bool
authenticationOptions authenticationOptions
}
func (o *setupOptions) addFlags(f *pflag.FlagSet) {
func (o *setupOptions) register(f *pflag.FlagSet) {
f.SortFlags = false
f.StringVar(&o.IssuerURL, "oidc-issuer-url", "", "Issuer URL of the provider")
f.StringVar(&o.ClientID, "oidc-client-id", "", "Client ID of the provider")
f.StringVar(&o.ClientSecret, "oidc-client-secret", "", "Client secret of the provider")
f.StringSliceVar(&o.ExtraScopes, "oidc-extra-scope", nil, "Scopes to request to the provider")
f.BoolVar(&o.UsePKCE, "oidc-use-pkce", false, "Force PKCE usage")
o.tlsOptions.addFlags(f)
o.authenticationOptions.addFlags(f)
f.StringVar(&o.CACertFilename, "certificate-authority", "", "Path to a cert file for the certificate authority")
f.StringVar(&o.CACertData, "certificate-authority-data", "", "Base64 encoded data for the certificate authority")
f.BoolVar(&o.SkipTLSVerify, "insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
o.authenticationOptions.register(f)
}
type Setup struct {
@@ -42,16 +44,17 @@ func (cmd *Setup) New() *cobra.Command {
RunE: func(c *cobra.Command, _ []string) error {
grantOptionSet, err := o.authenticationOptions.grantOptionSet()
if err != nil {
return fmt.Errorf("setup: %w", err)
return xerrors.Errorf("setup: %w", err)
}
in := setup.Stage2Input{
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
UsePKCE: o.UsePKCE,
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
IssuerURL: o.IssuerURL,
ClientID: o.ClientID,
ClientSecret: o.ClientSecret,
ExtraScopes: o.ExtraScopes,
CACertFilename: o.CACertFilename,
CACertData: o.CACertData,
SkipTLSVerify: o.SkipTLSVerify,
GrantOptionSet: grantOptionSet,
}
if c.Flags().Lookup("listen-address").Changed {
in.ListenAddressArgs = o.authenticationOptions.ListenAddress
@@ -61,12 +64,11 @@ func (cmd *Setup) New() *cobra.Command {
return nil
}
if err := cmd.Setup.DoStage2(c.Context(), in); err != nil {
return fmt.Errorf("setup: %w", err)
return xerrors.Errorf("setup: %w", err)
}
return nil
},
}
c.Flags().SortFlags = false
o.addFlags(c.Flags())
o.register(c.Flags())
return c
}

View File

@@ -1,24 +1,32 @@
// Package writer provides a writer for a credential plugin.
package writer
// Package credentialpluginwriter provides a writer for a credential plugin.
package credentialpluginwriter
import (
"encoding/json"
"fmt"
"time"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/credentialplugin"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
"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 credentialplugin.Output) error
Write(out Output) error
}
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}
type Writer struct {
@@ -26,7 +34,7 @@ type Writer struct {
}
// Write writes the ExecCredential to standard output for kubectl.
func (w *Writer) Write(out credentialplugin.Output) error {
func (w *Writer) Write(out Output) error {
ec := &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1beta1",
@@ -39,7 +47,7 @@ func (w *Writer) Write(out credentialplugin.Output) error {
}
e := json.NewEncoder(w.Stdout)
if err := e.Encode(ec); err != nil {
return fmt.Errorf("could not write the ExecCredential: %w", err)
return xerrors.Errorf("could not write the ExecCredential: %w", err)
}
return nil
}

View File

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

View File

@@ -1,5 +1,23 @@
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
@@ -21,3 +39,7 @@ type AuthProvider struct {
IDToken string // (optional) id-token
RefreshToken string // (optional) refresh-token
}
type Kubeconfig struct {
Logger logger.Interface
}

View File

@@ -1,35 +1,21 @@
package loader
package kubeconfig
import (
"errors"
"fmt"
"strings"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
var Set = wire.NewSet(
wire.Struct(new(Loader), "*"),
wire.Bind(new(Interface), new(*Loader)),
)
type Interface interface {
GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error)
}
type Loader struct{}
func (Loader) GetCurrentAuthProvider(explicitFilename string, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
func (*Kubeconfig) GetCurrentAuthProvider(explicitFilename string, contextName ContextName, userName UserName) (*AuthProvider, error) {
config, err := loadByDefaultRules(explicitFilename)
if err != nil {
return nil, fmt.Errorf("could not load the kubeconfig: %w", err)
return nil, xerrors.Errorf("could not load the kubeconfig: %w", err)
}
auth, err := findCurrentAuthProvider(config, contextName, userName)
if err != nil {
return nil, fmt.Errorf("could not find the current auth provider: %w", err)
return nil, xerrors.Errorf("could not find the current auth provider: %w", err)
}
return auth, nil
}
@@ -39,7 +25,7 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
rules.ExplicitPath = explicitFilename
config, err := rules.Load()
if err != nil {
return nil, fmt.Errorf("load error: %w", err)
return nil, xerrors.Errorf("load error: %w", err)
}
return config, err
}
@@ -48,29 +34,29 @@ func loadByDefaultRules(explicitFilename string) (*api.Config, error) {
// If contextName is given, this returns the user of the context.
// If userName is given, this ignores the context and returns the user.
// If any context or user is not found, this returns an error.
func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextName, userName kubeconfig.UserName) (*kubeconfig.AuthProvider, error) {
func findCurrentAuthProvider(config *api.Config, contextName ContextName, userName UserName) (*AuthProvider, error) {
if userName == "" {
if contextName == "" {
contextName = kubeconfig.ContextName(config.CurrentContext)
contextName = ContextName(config.CurrentContext)
}
contextNode, ok := config.Contexts[string(contextName)]
if !ok {
return nil, fmt.Errorf("context %s does not exist", contextName)
return nil, xerrors.Errorf("context %s does not exist", contextName)
}
userName = kubeconfig.UserName(contextNode.AuthInfo)
userName = UserName(contextNode.AuthInfo)
}
userNode, ok := config.AuthInfos[string(userName)]
if !ok {
return nil, fmt.Errorf("user %s does not exist", userName)
return nil, xerrors.Errorf("user %s does not exist", userName)
}
if userNode.AuthProvider == nil {
return nil, errors.New("auth-provider is missing")
return nil, xerrors.New("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return nil, fmt.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
return nil, xerrors.Errorf("auth-provider.name must be oidc but is %s", userNode.AuthProvider.Name)
}
if userNode.AuthProvider.Config == nil {
return nil, errors.New("auth-provider.config is missing")
return nil, xerrors.New("auth-provider.config is missing")
}
m := userNode.AuthProvider.Config
@@ -78,7 +64,7 @@ func findCurrentAuthProvider(config *api.Config, contextName kubeconfig.ContextN
if m["extra-scopes"] != "" {
extraScopes = strings.Split(m["extra-scopes"], ",")
}
return &kubeconfig.AuthProvider{
return &AuthProvider{
LocationOfOrigin: userNode.LocationOfOrigin,
UserName: userName,
ContextName: contextName,

View File

@@ -1,17 +1,17 @@
package loader
package kubeconfig
import (
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/kubeconfig"
"k8s.io/client-go/tools/clientcmd/api"
)
func Test_loadByDefaultRules(t *testing.T) {
t.Run("google.yaml>keycloak.yaml", func(t *testing.T) {
t.Setenv("KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
setenv(t, "KUBECONFIG", "testdata/kubeconfig.google.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.keycloak.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := loadByDefaultRules("")
if err != nil {
@@ -35,7 +35,8 @@ func Test_loadByDefaultRules(t *testing.T) {
})
t.Run("keycloak.yaml>google.yaml", func(t *testing.T) {
t.Setenv("KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
setenv(t, "KUBECONFIG", "testdata/kubeconfig.keycloak.yaml"+string(os.PathListSeparator)+"testdata/kubeconfig.google.yaml")
defer unsetenv(t, "KUBECONFIG")
config, err := loadByDefaultRules("")
if err != nil {
@@ -59,6 +60,20 @@ func Test_loadByDefaultRules(t *testing.T) {
})
}
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)
}
}
func Test_findCurrentAuthProvider(t *testing.T) {
t.Run("CurrentContext", func(t *testing.T) {
got, err := findCurrentAuthProvider(&api.Config{
@@ -90,7 +105,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.AuthProvider{
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
@@ -130,7 +145,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.AuthProvider{
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
ContextName: "theContext",
@@ -158,7 +173,7 @@ func Test_findCurrentAuthProvider(t *testing.T) {
if err != nil {
t.Fatalf("Could not find the current auth: %s", err)
}
want := &kubeconfig.AuthProvider{
want := &AuthProvider{
LocationOfOrigin: "/path/to/kubeconfig",
UserName: "theUser",
IDPIssuerURL: "https://accounts.google.com",

View File

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

View File

@@ -1,48 +1,35 @@
package writer
package kubeconfig
import (
"fmt"
"strings"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/kubeconfig"
"golang.org/x/xerrors"
"k8s.io/client-go/tools/clientcmd"
)
var Set = wire.NewSet(
wire.Struct(new(Writer), "*"),
wire.Bind(new(Interface), new(*Writer)),
)
type Interface interface {
UpdateAuthProvider(p kubeconfig.AuthProvider) error
}
type Writer struct{}
func (Writer) UpdateAuthProvider(p kubeconfig.AuthProvider) error {
func (*Kubeconfig) UpdateAuthProvider(p *AuthProvider) error {
config, err := clientcmd.LoadFromFile(p.LocationOfOrigin)
if err != nil {
return fmt.Errorf("could not load %s: %w", p.LocationOfOrigin, err)
return xerrors.Errorf("could not load %s: %w", p.LocationOfOrigin, err)
}
userNode, ok := config.AuthInfos[string(p.UserName)]
if !ok {
return fmt.Errorf("user %s does not exist", p.UserName)
return xerrors.Errorf("user %s does not exist", p.UserName)
}
if userNode.AuthProvider == nil {
return fmt.Errorf("auth-provider is missing")
return xerrors.Errorf("auth-provider is missing")
}
if userNode.AuthProvider.Name != "oidc" {
return fmt.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
return xerrors.Errorf("auth-provider must be oidc but is %s", userNode.AuthProvider.Name)
}
copyAuthProviderConfig(p, userNode.AuthProvider.Config)
if err := clientcmd.WriteToFile(*config, p.LocationOfOrigin); err != nil {
return fmt.Errorf("could not update %s: %w", p.LocationOfOrigin, err)
return xerrors.Errorf("could not update %s: %w", p.LocationOfOrigin, err)
}
return nil
}
func copyAuthProviderConfig(p kubeconfig.AuthProvider, m map[string]string) {
func copyAuthProviderConfig(p *AuthProvider, m map[string]string) {
setOrDeleteKey(m, "idp-issuer-url", p.IDPIssuerURL)
setOrDeleteKey(m, "client-id", p.ClientID)
setOrDeleteKey(m, "client-secret", p.ClientSecret)

View File

@@ -1,22 +1,25 @@
package writer
package kubeconfig
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/kubeconfig"
)
func TestKubeconfig_UpdateAuth(t *testing.T) {
var w Writer
var k Kubeconfig
t.Run("MinimumKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
if err := w.UpdateAuthProvider(kubeconfig.AuthProvider{
LocationOfOrigin: f,
defer func() {
if err := os.Remove(f.Name()); err != nil {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
@@ -26,7 +29,7 @@ func TestKubeconfig_UpdateAuth(t *testing.T) {
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
b, err := ioutil.ReadFile(f)
b, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatalf("Could not read kubeconfig: %s", err)
}
@@ -57,8 +60,13 @@ users:
t.Run("FullKeys", func(t *testing.T) {
f := newKubeconfigFile(t)
if err := w.UpdateAuthProvider(kubeconfig.AuthProvider{
LocationOfOrigin: f,
defer func() {
if err := os.Remove(f.Name()); err != nil {
t.Errorf("Could not remove the temp file: %s", err)
}
}()
if err := k.UpdateAuthProvider(&AuthProvider{
LocationOfOrigin: f.Name(),
UserName: "google",
IDPIssuerURL: "https://accounts.google.com",
ClientID: "GOOGLE_CLIENT_ID",
@@ -71,7 +79,7 @@ users:
}); err != nil {
t.Fatalf("Could not update auth: %s", err)
}
b, err := ioutil.ReadFile(f)
b, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Fatalf("Could not read kubeconfig: %s", err)
}
@@ -104,8 +112,8 @@ users:
})
}
const kubeconfigContent = `
apiVersion: v1
func newKubeconfigFile(t *testing.T) *os.File {
content := `apiVersion: v1
clusters: []
kind: Config
preferences: {}
@@ -115,12 +123,13 @@ users:
auth-provider:
config:
idp-issuer-url: https://accounts.google.com
name: oidc
`
func newKubeconfigFile(t *testing.T) string {
f := filepath.Join(t.TempDir(), "kubeconfig")
if err := os.WriteFile(f, []byte(kubeconfigContent), 0644); err != nil {
name: oidc`
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatalf("Could not create a file: %s", err)
}
defer f.Close()
if _, err := f.Write([]byte(content)); err != nil {
t.Fatalf("Could not write kubeconfig: %s", err)
}
return f

View File

@@ -7,7 +7,7 @@ import (
"github.com/google/wire"
"github.com/spf13/pflag"
"k8s.io/klog/v2"
"k8s.io/klog"
)
// Set provides an implementation and interface for Logger.
@@ -56,5 +56,5 @@ func (*Logger) V(level int) Verbose {
// IsEnabled returns true if the level is enabled.
func (*Logger) IsEnabled(level int) bool {
return klog.V(klog.Level(level)).Enabled()
return bool(klog.V(klog.Level(level)))
}

View File

@@ -0,0 +1,93 @@
// Package oidcclient provides a client of OpenID Connect.
package oidcclient
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"github.com/coreos/go-oidc"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient/logging"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
var Set = wire.NewSet(
wire.Struct(new(Factory), "*"),
wire.Bind(new(FactoryInterface), new(*Factory)),
)
type FactoryInterface interface {
New(ctx context.Context, config Config) (Interface, error)
}
// Config represents a configuration of OpenID Connect client.
type Config struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string // optional
CertPool certpool.Interface
SkipTLSVerify bool
}
type Factory struct {
Clock clock.Interface
Logger logger.Interface
}
// New returns an instance of adaptors.Interface with the given configuration.
func (f *Factory) New(ctx context.Context, config Config) (Interface, error) {
var tlsConfig tls.Config
tlsConfig.InsecureSkipVerify = config.SkipTLSVerify
config.CertPool.SetRootCAs(&tlsConfig)
baseTransport := &http.Transport{
TLSClientConfig: &tlsConfig,
Proxy: http.ProxyFromEnvironment,
}
loggingTransport := &logging.Transport{
Base: baseTransport,
Logger: f.Logger,
}
httpClient := &http.Client{
Transport: loggingTransport,
}
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
provider, err := oidc.NewProvider(ctx, config.IssuerURL)
if err != nil {
return nil, xerrors.Errorf("oidc discovery error: %w", err)
}
supportedPKCEMethods, err := extractSupportedPKCEMethods(provider)
if err != nil {
return nil, xerrors.Errorf("could not determine supported PKCE methods: %w", err)
}
return &client{
httpClient: httpClient,
provider: provider,
oauth2Config: oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: append(config.ExtraScopes, oidc.ScopeOpenID),
},
clock: f.Clock,
logger: f.Logger,
supportedPKCEMethods: supportedPKCEMethods,
}, nil
}
func extractSupportedPKCEMethods(provider *oidc.Provider) ([]string, error) {
var d struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
if err := provider.Claims(&d); err != nil {
return nil, fmt.Errorf("invalid discovery document: %w", err)
}
return d.CodeChallengeMethodsSupported, nil
}

View File

@@ -4,7 +4,7 @@ import (
"net/http"
"net/http/httputil"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/adaptors/logger"
)
const (

View File

@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/golang/mock/gomock"
"github.com/int128/kubelogin/pkg/testing/logger"
)
@@ -21,6 +22,9 @@ func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
}
func TestLoggingTransport_RoundTrip(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
req := httptest.NewRequest("GET", "http://example.com/hello", nil)
resp, err := http.ReadResponse(bufio.NewReader(strings.NewReader(`HTTP/1.1 200 OK
Host: example.com

View File

@@ -0,0 +1,123 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/oidcclient (interfaces: Interface)
// Package mock_oidcclient is a generated GoMock package.
package mock_oidcclient
import (
context "context"
gomock "github.com/golang/mock/gomock"
oidcclient "github.com/int128/kubelogin/pkg/adaptors/oidcclient"
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
}
// ExchangeAuthCode mocks base method.
func (m *MockInterface) ExchangeAuthCode(arg0 context.Context, arg1 oidcclient.ExchangeAuthCodeInput) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExchangeAuthCode", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExchangeAuthCode indicates an expected call of ExchangeAuthCode.
func (mr *MockInterfaceMockRecorder) ExchangeAuthCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeAuthCode", reflect.TypeOf((*MockInterface)(nil).ExchangeAuthCode), arg0, arg1)
}
// GetAuthCodeURL mocks base method.
func (m *MockInterface) GetAuthCodeURL(arg0 oidcclient.AuthCodeURLInput) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthCodeURL", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// GetAuthCodeURL indicates an expected call of GetAuthCodeURL.
func (mr *MockInterfaceMockRecorder) GetAuthCodeURL(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthCodeURL", reflect.TypeOf((*MockInterface)(nil).GetAuthCodeURL), arg0)
}
// GetTokenByAuthCode mocks base method.
func (m *MockInterface) GetTokenByAuthCode(arg0 context.Context, arg1 oidcclient.GetTokenByAuthCodeInput, arg2 chan<- string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByAuthCode", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByAuthCode indicates an expected call of GetTokenByAuthCode.
func (mr *MockInterfaceMockRecorder) GetTokenByAuthCode(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByAuthCode", reflect.TypeOf((*MockInterface)(nil).GetTokenByAuthCode), arg0, arg1, arg2)
}
// GetTokenByROPC mocks base method.
func (m *MockInterface) GetTokenByROPC(arg0 context.Context, arg1, arg2 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTokenByROPC", arg0, arg1, arg2)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTokenByROPC indicates an expected call of GetTokenByROPC.
func (mr *MockInterfaceMockRecorder) GetTokenByROPC(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTokenByROPC", reflect.TypeOf((*MockInterface)(nil).GetTokenByROPC), arg0, arg1, arg2)
}
// Refresh mocks base method.
func (m *MockInterface) Refresh(arg0 context.Context, arg1 string) (*oidcclient.TokenSet, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Refresh", arg0, arg1)
ret0, _ := ret[0].(*oidcclient.TokenSet)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Refresh indicates an expected call of Refresh.
func (mr *MockInterfaceMockRecorder) Refresh(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockInterface)(nil).Refresh), arg0, arg1)
}
// SupportedPKCEMethods mocks base method.
func (m *MockInterface) SupportedPKCEMethods() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SupportedPKCEMethods")
ret0, _ := ret[0].([]string)
return ret0
}
// SupportedPKCEMethods indicates an expected call of SupportedPKCEMethods.
func (mr *MockInterfaceMockRecorder) SupportedPKCEMethods() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedPKCEMethods", reflect.TypeOf((*MockInterface)(nil).SupportedPKCEMethods))
}

View File

@@ -1,26 +1,28 @@
package client
package oidcclient
import (
"context"
"fmt"
"net/http"
"time"
gooidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/pkce"
"github.com/coreos/go-oidc"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/domain/jwt"
"github.com/int128/kubelogin/pkg/domain/pkce"
"github.com/int128/oauth2cli"
"golang.org/x/oauth2"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_oidcclient/mock_oidcclient.go github.com/int128/kubelogin/pkg/adaptors/oidcclient Interface
type Interface interface {
GetAuthCodeURL(in AuthCodeURLInput) string
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error)
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error)
GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error)
ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error)
GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error)
GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error)
Refresh(ctx context.Context, refreshToken string) (*TokenSet, error)
SupportedPKCEMethods() []string
}
@@ -46,14 +48,19 @@ type GetTokenByAuthCodeInput struct {
PKCEParams pkce.Params
RedirectURLHostname string
AuthRequestExtraParams map[string]string
LocalServerSuccessHTML string
LocalServerCertFile string
LocalServerKeyFile string
}
// TokenSet represents an output DTO of
// Interface.GetTokenByAuthCode, Interface.GetTokenByROPC and Interface.Refresh.
type TokenSet struct {
IDToken string
RefreshToken string
IDTokenClaims jwt.Claims
}
type client struct {
httpClient *http.Client
provider *gooidc.Provider
provider *oidc.Provider
oauth2Config oauth2.Config
clock clock.Interface
logger logger.Interface
@@ -68,7 +75,7 @@ func (c *client) wrapContext(ctx context.Context) context.Context {
}
// GetTokenByAuthCode performs the authorization code flow.
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*oidc.TokenSet, error) {
func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeInput, localServerReadyChan chan<- string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
config := oauth2cli.Config{
OAuth2Config: c.oauth2Config,
@@ -78,14 +85,10 @@ func (c *client) GetTokenByAuthCode(ctx context.Context, in GetTokenByAuthCodeIn
LocalServerBindAddress: in.BindAddress,
LocalServerReadyChan: localServerReadyChan,
RedirectURLHostname: in.RedirectURLHostname,
LocalServerSuccessHTML: in.LocalServerSuccessHTML,
LocalServerCertFile: in.LocalServerCertFile,
LocalServerKeyFile: in.LocalServerKeyFile,
Logf: c.logger.V(1).Infof,
}
token, err := oauth2cli.GetToken(ctx, config)
if err != nil {
return nil, fmt.Errorf("oauth2 error: %w", err)
return nil, xerrors.Errorf("oauth2 error: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
@@ -99,14 +102,14 @@ func (c *client) GetAuthCodeURL(in AuthCodeURLInput) string {
}
// ExchangeAuthCode exchanges the authorization code and token.
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*oidc.TokenSet, error) {
func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
cfg := c.oauth2Config
cfg.RedirectURL = in.RedirectURI
opts := tokenRequestOptions(in.PKCEParams)
token, err := cfg.Exchange(ctx, in.Code, opts...)
if err != nil {
return nil, fmt.Errorf("exchange error: %w", err)
return nil, xerrors.Errorf("exchange error: %w", err)
}
return c.verifyToken(ctx, token, in.Nonce)
}
@@ -114,7 +117,7 @@ func (c *client) ExchangeAuthCode(ctx context.Context, in ExchangeAuthCodeInput)
func authorizationRequestOptions(n string, p pkce.Params, e map[string]string) []oauth2.AuthCodeOption {
o := []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
gooidc.Nonce(n),
oidc.Nonce(n),
}
if !p.IsZero() {
o = append(o,
@@ -142,17 +145,17 @@ func (c *client) SupportedPKCEMethods() []string {
}
// GetTokenByROPC performs the resource owner password credentials flow.
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*oidc.TokenSet, error) {
func (c *client) GetTokenByROPC(ctx context.Context, username, password string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
token, err := c.oauth2Config.PasswordCredentialsToken(ctx, username, password)
if err != nil {
return nil, fmt.Errorf("resource owner password credentials flow error: %w", err)
return nil, xerrors.Errorf("resource owner password credentials flow error: %w", err)
}
return c.verifyToken(ctx, token, "")
}
// Refresh sends a refresh token request and returns a token set.
func (c *client) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenSet, error) {
func (c *client) Refresh(ctx context.Context, refreshToken string) (*TokenSet, error) {
ctx = c.wrapContext(ctx)
currentToken := &oauth2.Token{
Expiry: time.Now(),
@@ -161,28 +164,37 @@ func (c *client) Refresh(ctx context.Context, refreshToken string) (*oidc.TokenS
source := c.oauth2Config.TokenSource(ctx, currentToken)
token, err := source.Token()
if err != nil {
return nil, fmt.Errorf("could not refresh the token: %w", err)
return nil, xerrors.Errorf("could not refresh the token: %w", err)
}
return c.verifyToken(ctx, token, "")
}
// verifyToken verifies the token with the certificates of the provider and the nonce.
// If the nonce is an empty string, it does not verify the nonce.
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*oidc.TokenSet, error) {
func (c *client) verifyToken(ctx context.Context, token *oauth2.Token, nonce string) (*TokenSet, error) {
idToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, fmt.Errorf("id_token is missing in the token response: %s", token)
return nil, xerrors.Errorf("id_token is missing in the token response: %s", token)
}
verifier := c.provider.Verifier(&gooidc.Config{ClientID: c.oauth2Config.ClientID, Now: c.clock.Now})
verifier := c.provider.Verifier(&oidc.Config{ClientID: c.oauth2Config.ClientID, Now: c.clock.Now})
verifiedIDToken, err := verifier.Verify(ctx, idToken)
if err != nil {
return nil, fmt.Errorf("could not verify the ID token: %w", err)
return nil, xerrors.Errorf("could not verify the ID token: %w", err)
}
if nonce != "" && nonce != verifiedIDToken.Nonce {
return nil, fmt.Errorf("nonce did not match (wants %s but got %s)", nonce, verifiedIDToken.Nonce)
return nil, xerrors.Errorf("nonce did not match (wants %s but got %s)", nonce, verifiedIDToken.Nonce)
}
return &oidc.TokenSet{
IDToken: idToken,
pretty, err := jwt.DecodePayloadAsPrettyJSON(idToken)
if err != nil {
return nil, xerrors.Errorf("could not decode the token: %w", err)
}
return &TokenSet{
IDToken: idToken,
IDTokenClaims: jwt.Claims{
Subject: verifiedIDToken.Subject,
Expiry: verifiedIDToken.Expiry,
Pretty: pretty,
},
RefreshToken: token.RefreshToken,
}, nil
}

View File

@@ -0,0 +1,63 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/int128/kubelogin/pkg/adaptors/reader (interfaces: Interface)
// Package mock_reader is a generated GoMock package.
package mock_reader
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
}
// ReadPassword mocks base method
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadPassword", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadPassword indicates an expected call of ReadPassword
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
}
// ReadString mocks base method
func (m *MockInterface) ReadString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadString", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadString indicates an expected call of ReadString
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)
}

View File

@@ -9,10 +9,13 @@ import (
"syscall"
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
"golang.org/x/term"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_reader/mock_reader.go github.com/int128/kubelogin/pkg/adaptors/reader Interface
// Set provides an implementation and interface for Reader.
var Set = wire.NewSet(
wire.Struct(new(Reader), "*"),
@@ -31,12 +34,12 @@ type Reader struct {
// ReadString reads a string from the stdin.
func (x *Reader) ReadString(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", fmt.Errorf("write error: %w", err)
return "", xerrors.Errorf("write error: %w", err)
}
r := bufio.NewReader(x.Stdin)
s, err := r.ReadString('\n')
if err != nil {
return "", fmt.Errorf("read error: %w", err)
return "", xerrors.Errorf("read error: %w", err)
}
s = strings.TrimRight(s, "\r\n")
return s, nil
@@ -45,14 +48,14 @@ func (x *Reader) ReadString(prompt string) (string, error) {
// ReadPassword reads a password from the stdin without echo back.
func (*Reader) ReadPassword(prompt string) (string, error) {
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
return "", fmt.Errorf("write error: %w", err)
return "", xerrors.Errorf("write error: %w", err)
}
b, err := term.ReadPassword(int(syscall.Stdin))
b, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", fmt.Errorf("read error: %w", err)
return "", xerrors.Errorf("read error: %w", err)
}
if _, err := fmt.Fprintln(os.Stderr); err != nil {
return "", fmt.Errorf("write error: %w", err)
return "", xerrors.Errorf("write error: %w", err)
}
return string(b), nil
}

View File

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

View File

@@ -0,0 +1,97 @@
package tokencache
import (
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"github.com/google/wire"
"golang.org/x/xerrors"
)
//go:generate mockgen -destination mock_tokencache/mock_tokencache.go github.com/int128/kubelogin/pkg/adaptors/tokencache Interface
// Set provides an implementation and interface for Kubeconfig.
var Set = wire.NewSet(
wire.Struct(new(Repository), "*"),
wire.Bind(new(Interface), new(*Repository)),
)
type Interface interface {
FindByKey(dir string, key Key) (*Value, error)
Save(dir string, key Key, value Value) error
}
// Key represents a key of a token cache.
type Key struct {
IssuerURL string
ClientID string
ClientSecret string
ExtraScopes []string
CACertFilename string
CACertData string
SkipTLSVerify bool
}
// Value represents a value of a token cache.
type Value struct {
IDToken string `json:"id_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}
// Repository provides access to the token cache on the local filesystem.
// Filename of a token cache is sha256 digest of the issuer, zero-character and client ID.
type Repository struct{}
func (r *Repository) FindByKey(dir string, key Key) (*Value, error) {
filename, err := computeFilename(key)
if err != nil {
return nil, xerrors.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.Open(p)
if err != nil {
return nil, xerrors.Errorf("could not open file %s: %w", p, err)
}
defer f.Close()
d := json.NewDecoder(f)
var c Value
if err := d.Decode(&c); err != nil {
return nil, xerrors.Errorf("invalid json file %s: %w", p, err)
}
return &c, nil
}
func (r *Repository) Save(dir string, key Key, value Value) error {
if err := os.MkdirAll(dir, 0700); err != nil {
return xerrors.Errorf("could not create directory %s: %w", dir, err)
}
filename, err := computeFilename(key)
if err != nil {
return xerrors.Errorf("could not compute the key: %w", err)
}
p := filepath.Join(dir, filename)
f, err := os.OpenFile(p, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return xerrors.Errorf("could not create file %s: %w", p, err)
}
defer f.Close()
e := json.NewEncoder(f)
if err := e.Encode(&value); err != nil {
return xerrors.Errorf("json encode error: %w", err)
}
return nil
}
func computeFilename(key Key) (string, error) {
s := sha256.New()
e := gob.NewEncoder(s)
if err := e.Encode(&key); err != nil {
return "", xerrors.Errorf("could not encode the key: %w", err)
}
h := hex.EncodeToString(s.Sum(nil))
return h, nil
}

View File

@@ -1,21 +1,28 @@
package repository
package tokencache
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/tokencache"
)
func TestRepository_FindByKey(t *testing.T) {
var r Repository
t.Run("Success", func(t *testing.T) {
dir := t.TempDir()
key := tokencache.Key{
dir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a temp dir: %s", err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("could not clean up the temp dir: %s", err)
}
}()
key := Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
@@ -33,12 +40,12 @@ func TestRepository_FindByKey(t *testing.T) {
t.Fatalf("could not write to the temp file: %s", err)
}
got, err := r.FindByKey(dir, key)
value, err := r.FindByKey(dir, key)
if err != nil {
t.Errorf("err wants nil but %+v", err)
}
want := &oidc.TokenSet{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := cmp.Diff(want, got); diff != "" {
want := &Value{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if diff := cmp.Diff(want, value); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
@@ -48,8 +55,17 @@ func TestRepository_Save(t *testing.T) {
var r Repository
t.Run("Success", func(t *testing.T) {
dir := t.TempDir()
key := tokencache.Key{
dir, err := ioutil.TempDir("", "kube")
if err != nil {
t.Fatalf("could not create a temp dir: %s", err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
t.Errorf("could not clean up the temp dir: %s", err)
}
}()
key := Key{
IssuerURL: "YOUR_ISSUER",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
@@ -57,8 +73,8 @@ func TestRepository_Save(t *testing.T) {
CACertFilename: "/path/to/cert",
SkipTLSVerify: false,
}
tokenSet := oidc.TokenSet{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, tokenSet); err != nil {
value := Value{IDToken: "YOUR_ID_TOKEN", RefreshToken: "YOUR_REFRESH_TOKEN"}
if err := r.Save(dir, key, value); err != nil {
t.Errorf("err wants nil but %+v", err)
}

View File

@@ -1,104 +0,0 @@
package cmd
import (
"fmt"
"strings"
"time"
"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"
)
type authenticationOptions struct {
GrantType string
ListenAddress []string
ListenPort []int // deprecated
AuthenticationTimeoutSec int
SkipOpenBrowser bool
BrowserCommand string
LocalServerCertFile string
LocalServerKeyFile string
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.BrowserCommand, "browser-command", "", "[authcode] Command to open the browser")
f.IntVar(&o.AuthenticationTimeoutSec, "authentication-timeout-sec", defaultAuthenticationTimeoutSec, "[authcode] Timeout of authentication in seconds")
f.StringVar(&o.LocalServerCertFile, "local-server-cert", "", "[authcode] Certificate path for the local server")
f.StringVar(&o.LocalServerKeyFile, "local-server-key", "", "[authcode] Certificate key path for the local server")
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) expandHomedir() {
o.LocalServerCertFile = expandHomedir(o.LocalServerCertFile)
o.LocalServerKeyFile = expandHomedir(o.LocalServerKeyFile)
}
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,
BrowserCommand: o.BrowserCommand,
AuthenticationTimeout: time.Duration(o.AuthenticationTimeoutSec) * time.Second,
LocalServerCertFile: o.LocalServerCertFile,
LocalServerKeyFile: o.LocalServerKeyFile,
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 = fmt.Errorf("grant-type must be one of (%s)", allGrantType)
}
return
}

View File

@@ -1,143 +0,0 @@
package cmd
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"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"
)
func Test_authenticationOptions_grantOptionSet(t *testing.T) {
tests := map[string]struct {
args []string
want authentication.GrantOptionSet
}{
"NoFlag": {
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
"FullOptions": {
args: []string{
"--grant-type", "authcode",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--skip-open-browser",
"--browser-command", "firefox",
"--authentication-timeout-sec", "10",
"--local-server-cert", "/path/to/local-server-cert",
"--local-server-key", "/path/to/local-server-key",
"--open-url-after-authentication", "https://example.com/success.html",
"--oidc-redirect-url-hostname", "example",
"--oidc-auth-request-extra-params", "ttl=86400",
"--oidc-auth-request-extra-params", "reauth=true",
"--username", "USER",
"--password", "PASS",
},
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
SkipOpenBrowser: true,
BrowserCommand: "firefox",
AuthenticationTimeout: 10 * time.Second,
LocalServerCertFile: "/path/to/local-server-cert",
LocalServerKeyFile: "/path/to/local-server-key",
OpenURLAfterAuthentication: "https://example.com/success.html",
RedirectURLHostname: "example",
AuthRequestExtraParams: map[string]string{"ttl": "86400", "reauth": "true"},
},
},
},
"when --listen-port is set, it should convert the port to address": {
args: []string{
"--listen-port", "10080",
"--listen-port", "20080",
},
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
"when --listen-port is set, it should ignore --listen-address flags": {
args: []string{
"--listen-port", "10080",
"--listen-port", "20080",
"--listen-address", "127.0.0.1:30080",
"--listen-address", "127.0.0.1:40080",
},
want: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
"GrantType=authcode-keyboard": {
args: []string{
"--grant-type", "authcode-keyboard",
},
want: authentication.GrantOptionSet{
AuthCodeKeyboardOption: &authcode.KeyboardOption{},
},
},
"GrantType=password": {
args: []string{
"--grant-type", "password",
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
want: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
"GrantType=auto": {
args: []string{
"--listen-address", "127.0.0.1:10080",
"--listen-address", "127.0.0.1:20080",
"--username", "USER",
"--password", "PASS",
},
want: authentication.GrantOptionSet{
ROPCOption: &ropc.Option{
Username: "USER",
Password: "PASS",
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
var o authenticationOptions
f := pflag.NewFlagSet("", pflag.ContinueOnError)
o.addFlags(f)
if err := f.Parse(c.args); err != nil {
t.Fatalf("Parse error: %s", err)
}
got, err := o.grantOptionSet()
if err != nil {
t.Fatalf("grantOptionSet error: %s", err)
}
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -1,246 +0,0 @@
package cmd
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/int128/kubelogin/pkg/oidc"
"github.com/int128/kubelogin/pkg/testing/logger"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/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,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
"FullOptions": {
args: []string{executable,
"--kubeconfig", "/path/to/kubeconfig",
"--context", "hello.k8s.local",
"--user", "google",
"-v1",
},
in: standalone.Input{
KubeconfigFilename: "/path/to/kubeconfig",
KubeconfigContext: "hello.k8s.local",
KubeconfigUser: "google",
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctx := context.TODO()
mockStandalone := standalone.NewMockInterface(t)
mockStandalone.EXPECT().
Do(ctx, c.in).
Return(nil)
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) {
cmd := Cmd{
Root: &Root{
Standalone: standalone.NewMockInterface(t),
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) {
userHomeDir, err := os.UserHomeDir()
if err != nil {
t.Fatalf("os.UserHomeDir error: %s", err)
}
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: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
Provider: oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
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",
"-v1",
},
in: credentialplugin.Input{
TokenCacheDir: filepath.Join(userHomeDir, ".kube/cache/oidc-login"),
Provider: oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
ExtraScopes: []string{"email", "profile"},
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
},
},
},
},
"HomedirExpansion": {
args: []string{executable,
"get-token",
"--oidc-issuer-url", "https://issuer.example.com",
"--oidc-client-id", "YOUR_CLIENT_ID",
"--certificate-authority", "~/.kube/ca.crt",
"--local-server-cert", "~/.kube/oidc-server.crt",
"--local-server-key", "~/.kube/oidc-server.key",
"--token-cache-dir", "~/.kube/oidc-cache",
},
in: credentialplugin.Input{
TokenCacheDir: filepath.Join(userHomeDir, ".kube/oidc-cache"),
Provider: oidc.Provider{
IssuerURL: "https://issuer.example.com",
ClientID: "YOUR_CLIENT_ID",
},
GrantOptionSet: authentication.GrantOptionSet{
AuthCodeBrowserOption: &authcode.BrowserOption{
BindAddress: defaultListenAddress,
AuthenticationTimeout: defaultAuthenticationTimeoutSec * time.Second,
RedirectURLHostname: "localhost",
LocalServerCertFile: filepath.Join(userHomeDir, ".kube/oidc-server.crt"),
LocalServerKeyFile: filepath.Join(userHomeDir, ".kube/oidc-server.key"),
},
},
TLSClientConfig: tlsclientconfig.Config{
CACertFilename: []string{filepath.Join(userHomeDir, ".kube/ca.crt")},
},
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
ctx := context.TODO()
getToken := credentialplugin.NewMockInterface(t)
getToken.EXPECT().
Do(ctx, c.in).
Return(nil)
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) {
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: credentialplugin.NewMockInterface(t),
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) {
ctx := context.TODO()
cmd := Cmd{
Root: &Root{
Logger: logger.New(t),
},
GetToken: &GetToken{
GetToken: credentialplugin.NewMockInterface(t),
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,15 +0,0 @@
package cmd
import (
"path/filepath"
"strings"
"k8s.io/client-go/util/homedir"
)
func expandHomedir(s string) string {
if !strings.HasPrefix(s, "~") {
return s
}
return filepath.Join(homedir.HomeDir(), strings.TrimPrefix(s, "~"))
}

View File

@@ -1,76 +0,0 @@
// Code generated by mockery v2.13.1. DO NOT EDIT.
package cmd
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// MockInterface is an autogenerated mock type for the Interface type
type MockInterface struct {
mock.Mock
}
type MockInterface_Expecter struct {
mock *mock.Mock
}
func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
return &MockInterface_Expecter{mock: &_m.Mock}
}
// Run provides a mock function with given fields: ctx, args, version
func (_m *MockInterface) Run(ctx context.Context, args []string, version string) int {
ret := _m.Called(ctx, args, version)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, []string, string) int); ok {
r0 = rf(ctx, args, version)
} else {
r0 = ret.Get(0).(int)
}
return r0
}
// MockInterface_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
type MockInterface_Run_Call struct {
*mock.Call
}
// Run is a helper method to define mock.On call
// - ctx context.Context
// - args []string
// - version string
func (_e *MockInterface_Expecter) Run(ctx interface{}, args interface{}, version interface{}) *MockInterface_Run_Call {
return &MockInterface_Run_Call{Call: _e.mock.On("Run", ctx, args, version)}
}
func (_c *MockInterface_Run_Call) Run(run func(ctx context.Context, args []string, version string)) *MockInterface_Run_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].([]string), args[2].(string))
})
return _c
}
func (_c *MockInterface_Run_Call) Return(_a0 int) *MockInterface_Run_Call {
_c.Call.Return(_a0)
return _c
}
type mockConstructorTestingTNewMockInterface interface {
mock.TestingT
Cleanup(func())
}
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockInterface(t mockConstructorTestingTNewMockInterface) *MockInterface {
mock := &MockInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,73 +0,0 @@
package cmd
import (
"fmt"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/kubeconfig"
"github.com/int128/kubelogin/pkg/usecases/standalone"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
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 fmt.Errorf("invalid option: %w", err)
}
in := standalone.Input{
KubeconfigFilename: o.Kubeconfig,
KubeconfigContext: kubeconfig.ContextName(o.Context),
KubeconfigUser: kubeconfig.UserName(o.User),
GrantOptionSet: grantOptionSet,
TLSClientConfig: o.tlsOptions.tlsClientConfig(),
}
if err := cmd.Standalone.Do(c.Context(), in); err != nil {
return fmt.Errorf("login: %w", err)
}
return nil
},
}
c.Flags().SortFlags = false
o.addFlags(c.Flags())
cmd.Logger.AddFlags(c.PersistentFlags())
return c
}

View File

@@ -1,52 +0,0 @@
package cmd
import (
"crypto/tls"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/spf13/pflag"
)
type tlsOptions struct {
CACertFilename []string
CACertData []string
SkipTLSVerify bool
RenegotiateOnceAsClient bool
RenegotiateFreelyAsClient bool
}
func (o *tlsOptions) addFlags(f *pflag.FlagSet) {
f.StringArrayVar(&o.CACertFilename, "certificate-authority", nil, "Path to a cert file for the certificate authority")
f.StringArrayVar(&o.CACertData, "certificate-authority-data", nil, "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")
f.BoolVar(&o.RenegotiateOnceAsClient, "tls-renegotiation-once", false, "If set, allow a remote server to request renegotiation once per connection")
f.BoolVar(&o.RenegotiateFreelyAsClient, "tls-renegotiation-freely", false, "If set, allow a remote server to repeatedly request renegotiation")
}
func (o *tlsOptions) expandHomedir() {
var caCertFilenames []string
for _, caCertFilename := range o.CACertFilename {
expanded := expandHomedir(caCertFilename)
caCertFilenames = append(caCertFilenames, expanded)
}
o.CACertFilename = caCertFilenames
}
func (o tlsOptions) tlsClientConfig() tlsclientconfig.Config {
return tlsclientconfig.Config{
CACertFilename: o.CACertFilename,
CACertData: o.CACertData,
SkipTLSVerify: o.SkipTLSVerify,
Renegotiation: o.renegotiationSupport(),
}
}
func (o tlsOptions) renegotiationSupport() tls.RenegotiationSupport {
if o.RenegotiateOnceAsClient {
return tls.RenegotiateOnceAsClient
}
if o.RenegotiateFreelyAsClient {
return tls.RenegotiateFreelyAsClient
}
return tls.RenegotiateNever
}

View File

@@ -1,92 +0,0 @@
package cmd
import (
"crypto/tls"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/int128/kubelogin/pkg/tlsclientconfig"
"github.com/spf13/pflag"
)
func Test_tlsOptions_tlsClientConfig(t *testing.T) {
tests := map[string]struct {
args []string
want tlsclientconfig.Config
}{
"NoFlag": {},
"SkipTLSVerify": {
args: []string{
"--insecure-skip-tls-verify",
},
want: tlsclientconfig.Config{
SkipTLSVerify: true,
},
},
"CACertFilename1": {
args: []string{
"--certificate-authority", "/path/to/cert1",
},
want: tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert1"},
},
},
"CACertFilename2": {
args: []string{
"--certificate-authority", "/path/to/cert1",
"--certificate-authority", "/path/to/cert2",
},
want: tlsclientconfig.Config{
CACertFilename: []string{"/path/to/cert1", "/path/to/cert2"},
},
},
"CACertData1": {
args: []string{
"--certificate-authority-data", "base64encoded1",
},
want: tlsclientconfig.Config{
CACertData: []string{"base64encoded1"},
},
},
"CACertData2": {
args: []string{
"--certificate-authority-data", "base64encoded1",
"--certificate-authority-data", "base64encoded2",
},
want: tlsclientconfig.Config{
CACertData: []string{"base64encoded1", "base64encoded2"},
},
},
"RenegotiateOnceAsClient": {
args: []string{
"--tls-renegotiation-once",
},
want: tlsclientconfig.Config{
Renegotiation: tls.RenegotiateOnceAsClient,
},
},
"RenegotiateFreelyAsClient": {
args: []string{
"--tls-renegotiation-freely",
},
want: tlsclientconfig.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
},
}
for name, c := range tests {
t.Run(name, func(t *testing.T) {
var o tlsOptions
f := pflag.NewFlagSet("", pflag.ContinueOnError)
o.addFlags(f)
if err := f.Parse(c.args); err != nil {
t.Fatalf("Parse error: %s", err)
}
got := o.tlsClientConfig()
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
}
}

View File

@@ -1,10 +0,0 @@
// Package credentialplugin provides the types for client-go credential plugins.
package credentialplugin
import "time"
// Output represents an output object of the credential plugin.
type Output struct {
Token string
Expiry time.Time
}

View File

@@ -1,73 +0,0 @@
// Code generated by mockery v2.13.1. DO NOT EDIT.
package writer
import (
credentialplugin "github.com/int128/kubelogin/pkg/credentialplugin"
mock "github.com/stretchr/testify/mock"
)
// MockInterface is an autogenerated mock type for the Interface type
type MockInterface struct {
mock.Mock
}
type MockInterface_Expecter struct {
mock *mock.Mock
}
func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
return &MockInterface_Expecter{mock: &_m.Mock}
}
// Write provides a mock function with given fields: out
func (_m *MockInterface) Write(out credentialplugin.Output) error {
ret := _m.Called(out)
var r0 error
if rf, ok := ret.Get(0).(func(credentialplugin.Output) error); ok {
r0 = rf(out)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockInterface_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
type MockInterface_Write_Call struct {
*mock.Call
}
// Write is a helper method to define mock.On call
// - out credentialplugin.Output
func (_e *MockInterface_Expecter) Write(out interface{}) *MockInterface_Write_Call {
return &MockInterface_Write_Call{Call: _e.mock.On("Write", out)}
}
func (_c *MockInterface_Write_Call) Run(run func(out credentialplugin.Output)) *MockInterface_Write_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(credentialplugin.Output))
})
return _c
}
func (_c *MockInterface_Write_Call) Return(_a0 error) *MockInterface_Write_Call {
_c.Call.Return(_a0)
return _c
}
type mockConstructorTestingTNewMockInterface interface {
mock.TestingT
Cleanup(func())
}
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockInterface(t mockConstructorTestingTNewMockInterface) *MockInterface {
mock := &MockInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -5,26 +5,24 @@ package di
import (
"github.com/google/wire"
"github.com/int128/kubelogin/pkg/cmd"
"github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/mutex"
"github.com/int128/kubelogin/pkg/infrastructure/reader"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
kubeconfigLoader "github.com/int128/kubelogin/pkg/kubeconfig/loader"
kubeconfigWriter "github.com/int128/kubelogin/pkg/kubeconfig/writer"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/tlsclientconfig/loader"
"github.com/int128/kubelogin/pkg/tokencache/repository"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"github.com/int128/kubelogin/pkg/usecases/authentication"
"github.com/int128/kubelogin/pkg/usecases/credentialplugin"
"github.com/int128/kubelogin/pkg/usecases/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
)
// NewCmd returns an instance of infrastructure.Cmd.
// NewCmd returns an instance of adaptors.Cmd.
func NewCmd() cmd.Interface {
wire.Build(
NewCmdForHeadless,
@@ -38,7 +36,7 @@ func NewCmd() cmd.Interface {
return nil
}
// NewCmdForHeadless returns an instance of infrastructure.Cmd for headless testing.
// NewCmdForHeadless returns an instance of adaptors.Cmd for headless testing.
func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interface, browser.Interface) cmd.Interface {
wire.Build(
// use-cases
@@ -47,16 +45,14 @@ func NewCmdForHeadless(clock.Interface, stdio.Stdin, stdio.Stdout, logger.Interf
credentialplugin.Set,
setup.Set,
// infrastructure
// adaptors
cmd.Set,
reader.Set,
kubeconfigLoader.Set,
kubeconfigWriter.Set,
repository.Set,
client.Set,
loader.Set,
writer.Set,
mutex.Set,
kubeconfig.Set,
tokencache.Set,
oidcclient.Set,
certpool.Set,
credentialpluginwriter.Set,
)
return nil
}

View File

@@ -6,22 +6,18 @@
package di
import (
"github.com/int128/kubelogin/pkg/cmd"
writer2 "github.com/int128/kubelogin/pkg/credentialplugin/writer"
"github.com/int128/kubelogin/pkg/infrastructure/browser"
"github.com/int128/kubelogin/pkg/infrastructure/clock"
"github.com/int128/kubelogin/pkg/infrastructure/logger"
"github.com/int128/kubelogin/pkg/infrastructure/mutex"
"github.com/int128/kubelogin/pkg/infrastructure/reader"
"github.com/int128/kubelogin/pkg/infrastructure/stdio"
loader2 "github.com/int128/kubelogin/pkg/kubeconfig/loader"
"github.com/int128/kubelogin/pkg/kubeconfig/writer"
"github.com/int128/kubelogin/pkg/oidc/client"
"github.com/int128/kubelogin/pkg/tlsclientconfig/loader"
"github.com/int128/kubelogin/pkg/tokencache/repository"
"github.com/int128/kubelogin/pkg/adaptors/browser"
"github.com/int128/kubelogin/pkg/adaptors/certpool"
"github.com/int128/kubelogin/pkg/adaptors/clock"
"github.com/int128/kubelogin/pkg/adaptors/cmd"
"github.com/int128/kubelogin/pkg/adaptors/credentialpluginwriter"
"github.com/int128/kubelogin/pkg/adaptors/kubeconfig"
"github.com/int128/kubelogin/pkg/adaptors/logger"
"github.com/int128/kubelogin/pkg/adaptors/oidcclient"
"github.com/int128/kubelogin/pkg/adaptors/reader"
"github.com/int128/kubelogin/pkg/adaptors/stdio"
"github.com/int128/kubelogin/pkg/adaptors/tokencache"
"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/setup"
"github.com/int128/kubelogin/pkg/usecases/standalone"
@@ -46,59 +42,56 @@ var (
)
func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout stdio.Stdout, loggerInterface logger.Interface, browserInterface browser.Interface) cmd.Interface {
loaderLoader := loader.Loader{}
factory := &client.Factory{
Loader: loaderLoader,
factory := &oidcclient.Factory{
Clock: clockInterface,
Logger: loggerInterface,
}
authcodeBrowser := &authcode.Browser{
authCode := &authentication.AuthCode{
Browser: browserInterface,
Logger: loggerInterface,
}
readerReader := &reader.Reader{
Stdin: stdin,
}
keyboard := &authcode.Keyboard{
authCodeKeyboard := &authentication.AuthCodeKeyboard{
Reader: readerReader,
Logger: loggerInterface,
}
ropcROPC := &ropc.ROPC{
ropc := &authentication.ROPC{
Reader: readerReader,
Logger: loggerInterface,
}
authenticationAuthentication := &authentication.Authentication{
ClientFactory: factory,
OIDCClient: factory,
Logger: loggerInterface,
Clock: clockInterface,
AuthCodeBrowser: authcodeBrowser,
AuthCodeKeyboard: keyboard,
ROPC: ropcROPC,
AuthCode: authCode,
AuthCodeKeyboard: authCodeKeyboard,
ROPC: ropc,
}
loader3 := &loader2.Loader{}
writerWriter := &writer.Writer{}
kubeconfigKubeconfig := &kubeconfig.Kubeconfig{
Logger: loggerInterface,
}
newFunc := _wireNewFuncValue
standaloneStandalone := &standalone.Standalone{
Authentication: authenticationAuthentication,
KubeconfigLoader: loader3,
KubeconfigWriter: writerWriter,
Logger: loggerInterface,
Authentication: authenticationAuthentication,
Kubeconfig: kubeconfigKubeconfig,
NewCertPool: newFunc,
Logger: loggerInterface,
}
root := &cmd.Root{
Standalone: standaloneStandalone,
Logger: loggerInterface,
}
repositoryRepository := &repository.Repository{}
writer3 := &writer2.Writer{
repository := &tokencache.Repository{}
writer := &credentialpluginwriter.Writer{
Stdout: stdout,
}
mutexMutex := &mutex.Mutex{
Logger: loggerInterface,
}
getToken := &credentialplugin.GetToken{
Authentication: authenticationAuthentication,
TokenCacheRepository: repositoryRepository,
Writer: writer3,
Mutex: mutexMutex,
TokenCacheRepository: repository,
NewCertPool: newFunc,
Writer: writer,
Logger: loggerInterface,
}
cmdGetToken := &cmd.GetToken{
@@ -107,6 +100,7 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
}
setupSetup := &setup.Setup{
Authentication: authenticationAuthentication,
NewCertPool: newFunc,
Logger: loggerInterface,
}
cmdSetup := &cmd.Setup{
@@ -120,3 +114,7 @@ func NewCmdForHeadless(clockInterface clock.Interface, stdin stdio.Stdin, stdout
}
return cmdCmd
}
var (
_wireNewFuncValue = certpool.NewFunc(certpool.New)
)

View File

@@ -6,9 +6,10 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"golang.org/x/xerrors"
)
// DecodeWithoutVerify decodes the JWT string and returns the claims.
@@ -16,18 +17,18 @@ import (
func DecodeWithoutVerify(s string) (*Claims, error) {
payload, err := DecodePayloadAsRawJSON(s)
if err != nil {
return nil, fmt.Errorf("could not decode the payload: %w", err)
return nil, xerrors.Errorf("could not decode the payload: %w", err)
}
var claims struct {
Subject string `json:"sub,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
}
if err := json.NewDecoder(bytes.NewReader(payload)).Decode(&claims); err != nil {
return nil, fmt.Errorf("could not decode the json of token: %w", err)
return nil, xerrors.Errorf("could not decode the json of token: %w", err)
}
var prettyJson bytes.Buffer
if err := json.Indent(&prettyJson, payload, "", " "); err != nil {
return nil, fmt.Errorf("could not indent the json of token: %w", err)
return nil, xerrors.Errorf("could not indent the json of token: %w", err)
}
return &Claims{
Subject: claims.Subject,
@@ -40,11 +41,11 @@ func DecodeWithoutVerify(s string) (*Claims, error) {
func DecodePayloadAsPrettyJSON(s string) (string, error) {
payload, err := DecodePayloadAsRawJSON(s)
if err != nil {
return "", fmt.Errorf("could not decode the payload: %w", err)
return "", xerrors.Errorf("could not decode the payload: %w", err)
}
var prettyJson bytes.Buffer
if err := json.Indent(&prettyJson, payload, "", " "); err != nil {
return "", fmt.Errorf("could not indent the json of token: %w", err)
return "", xerrors.Errorf("could not indent the json of token: %w", err)
}
return prettyJson.String(), nil
}
@@ -53,11 +54,11 @@ func DecodePayloadAsPrettyJSON(s string) (string, error) {
func DecodePayloadAsRawJSON(s string) ([]byte, error) {
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return nil, fmt.Errorf("wants %d segments but got %d segments", 3, len(parts))
return nil, xerrors.Errorf("wants %d segments but got %d segments", 3, len(parts))
}
payloadJSON, err := decodePayload(parts[1])
if err != nil {
return nil, fmt.Errorf("could not decode the payload: %w", err)
return nil, xerrors.Errorf("could not decode the payload: %w", err)
}
return payloadJSON, nil
}
@@ -65,7 +66,7 @@ func DecodePayloadAsRawJSON(s string) ([]byte, error) {
func decodePayload(payload string) ([]byte, error) {
b, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(payload)
if err != nil {
return nil, fmt.Errorf("invalid base64: %w", err)
return nil, xerrors.Errorf("invalid base64: %w", err)
}
return b, nil
}

View File

@@ -4,7 +4,7 @@ import (
"testing"
"time"
"github.com/int128/kubelogin/pkg/jwt"
"github.com/int128/kubelogin/pkg/domain/jwt"
)
type timeProvider time.Time

37
pkg/domain/oidc/oidc.go Normal file
View File

@@ -0,0 +1,37 @@
package oidc
import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"golang.org/x/xerrors"
)
func NewState() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
func NewNonce() (string, error) {
b, err := random32()
if err != nil {
return "", xerrors.Errorf("could not generate a random: %w", err)
}
return base64URLEncode(b), nil
}
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, xerrors.Errorf("read error: %w", err)
}
return b, nil
}
func base64URLEncode(b []byte) string {
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b)
}

View File

@@ -7,14 +7,15 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"golang.org/x/xerrors"
)
var Plain Params
const (
// code challenge methods defined as https://tools.ietf.org/html/rfc7636#section-4.3
MethodS256 = "S256"
methodS256 = "S256"
)
// Params represents a set of the PKCE parameters.
@@ -33,7 +34,7 @@ func (p Params) IsZero() bool {
// It returns Plain if no method is available.
func New(methods []string) (Params, error) {
for _, method := range methods {
if method == MethodS256 {
if method == methodS256 {
return NewS256()
}
}
@@ -44,7 +45,7 @@ func New(methods []string) (Params, error) {
func NewS256() (Params, error) {
b, err := random32()
if err != nil {
return Plain, fmt.Errorf("could not generate a random: %w", err)
return Plain, xerrors.Errorf("could not generate a random: %w", err)
}
return computeS256(b), nil
}
@@ -52,7 +53,7 @@ func NewS256() (Params, error) {
func random32() ([]byte, error) {
b := make([]byte, 32)
if err := binary.Read(rand.Reader, binary.LittleEndian, b); err != nil {
return nil, fmt.Errorf("read error: %w", err)
return nil, xerrors.Errorf("read error: %w", err)
}
return b, nil
}
@@ -63,7 +64,7 @@ func computeS256(b []byte) Params {
_, _ = s.Write([]byte(v))
return Params{
CodeChallenge: base64URLEncode(s.Sum(nil)),
CodeChallengeMethod: MethodS256,
CodeChallengeMethod: methodS256,
CodeVerifier: v,
}
}

View File

@@ -1,113 +0,0 @@
// Code generated by mockery v2.13.1. DO NOT EDIT.
package browser
import (
context "context"
mock "github.com/stretchr/testify/mock"
)
// MockInterface is an autogenerated mock type for the Interface type
type MockInterface struct {
mock.Mock
}
type MockInterface_Expecter struct {
mock *mock.Mock
}
func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
return &MockInterface_Expecter{mock: &_m.Mock}
}
// Open provides a mock function with given fields: url
func (_m *MockInterface) Open(url string) error {
ret := _m.Called(url)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(url)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockInterface_Open_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Open'
type MockInterface_Open_Call struct {
*mock.Call
}
// Open is a helper method to define mock.On call
// - url string
func (_e *MockInterface_Expecter) Open(url interface{}) *MockInterface_Open_Call {
return &MockInterface_Open_Call{Call: _e.mock.On("Open", url)}
}
func (_c *MockInterface_Open_Call) Run(run func(url string)) *MockInterface_Open_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockInterface_Open_Call) Return(_a0 error) *MockInterface_Open_Call {
_c.Call.Return(_a0)
return _c
}
// OpenCommand provides a mock function with given fields: ctx, url, command
func (_m *MockInterface) OpenCommand(ctx context.Context, url string, command string) error {
ret := _m.Called(ctx, url, command)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, url, command)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockInterface_OpenCommand_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenCommand'
type MockInterface_OpenCommand_Call struct {
*mock.Call
}
// OpenCommand is a helper method to define mock.On call
// - ctx context.Context
// - url string
// - command string
func (_e *MockInterface_Expecter) OpenCommand(ctx interface{}, url interface{}, command interface{}) *MockInterface_OpenCommand_Call {
return &MockInterface_OpenCommand_Call{Call: _e.mock.On("OpenCommand", ctx, url, command)}
}
func (_c *MockInterface_OpenCommand_Call) Run(run func(ctx context.Context, url string, command string)) *MockInterface_OpenCommand_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(string))
})
return _c
}
func (_c *MockInterface_OpenCommand_Call) Return(_a0 error) *MockInterface_OpenCommand_Call {
_c.Call.Return(_a0)
return _c
}
type mockConstructorTestingTNewMockInterface interface {
mock.TestingT
Cleanup(func())
}
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockInterface(t mockConstructorTestingTNewMockInterface) *MockInterface {
mock := &MockInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,73 +0,0 @@
// Code generated by mockery v2.13.1. DO NOT EDIT.
package clock
import (
time "time"
mock "github.com/stretchr/testify/mock"
)
// MockInterface is an autogenerated mock type for the Interface type
type MockInterface struct {
mock.Mock
}
type MockInterface_Expecter struct {
mock *mock.Mock
}
func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
return &MockInterface_Expecter{mock: &_m.Mock}
}
// Now provides a mock function with given fields:
func (_m *MockInterface) Now() time.Time {
ret := _m.Called()
var r0 time.Time
if rf, ok := ret.Get(0).(func() time.Time); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(time.Time)
}
return r0
}
// MockInterface_Now_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Now'
type MockInterface_Now_Call struct {
*mock.Call
}
// Now is a helper method to define mock.On call
func (_e *MockInterface_Expecter) Now() *MockInterface_Now_Call {
return &MockInterface_Now_Call{Call: _e.mock.On("Now")}
}
func (_c *MockInterface_Now_Call) Run(run func()) *MockInterface_Now_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockInterface_Now_Call) Return(_a0 time.Time) *MockInterface_Now_Call {
_c.Call.Return(_a0)
return _c
}
type mockConstructorTestingTNewMockInterface interface {
mock.TestingT
Cleanup(func())
}
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockInterface(t mockConstructorTestingTNewMockInterface) *MockInterface {
mock := &MockInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -1,179 +0,0 @@
// Code generated by mockery v2.13.1. DO NOT EDIT.
package logger
import (
pflag "github.com/spf13/pflag"
mock "github.com/stretchr/testify/mock"
)
// MockInterface is an autogenerated mock type for the Interface type
type MockInterface struct {
mock.Mock
}
type MockInterface_Expecter struct {
mock *mock.Mock
}
func (_m *MockInterface) EXPECT() *MockInterface_Expecter {
return &MockInterface_Expecter{mock: &_m.Mock}
}
// AddFlags provides a mock function with given fields: f
func (_m *MockInterface) AddFlags(f *pflag.FlagSet) {
_m.Called(f)
}
// MockInterface_AddFlags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFlags'
type MockInterface_AddFlags_Call struct {
*mock.Call
}
// AddFlags is a helper method to define mock.On call
// - f *pflag.FlagSet
func (_e *MockInterface_Expecter) AddFlags(f interface{}) *MockInterface_AddFlags_Call {
return &MockInterface_AddFlags_Call{Call: _e.mock.On("AddFlags", f)}
}
func (_c *MockInterface_AddFlags_Call) Run(run func(f *pflag.FlagSet)) *MockInterface_AddFlags_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*pflag.FlagSet))
})
return _c
}
func (_c *MockInterface_AddFlags_Call) Return() *MockInterface_AddFlags_Call {
_c.Call.Return()
return _c
}
// IsEnabled provides a mock function with given fields: level
func (_m *MockInterface) IsEnabled(level int) bool {
ret := _m.Called(level)
var r0 bool
if rf, ok := ret.Get(0).(func(int) bool); ok {
r0 = rf(level)
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockInterface_IsEnabled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsEnabled'
type MockInterface_IsEnabled_Call struct {
*mock.Call
}
// IsEnabled is a helper method to define mock.On call
// - level int
func (_e *MockInterface_Expecter) IsEnabled(level interface{}) *MockInterface_IsEnabled_Call {
return &MockInterface_IsEnabled_Call{Call: _e.mock.On("IsEnabled", level)}
}
func (_c *MockInterface_IsEnabled_Call) Run(run func(level int)) *MockInterface_IsEnabled_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockInterface_IsEnabled_Call) Return(_a0 bool) *MockInterface_IsEnabled_Call {
_c.Call.Return(_a0)
return _c
}
// Printf provides a mock function with given fields: format, args
func (_m *MockInterface) Printf(format string, args ...interface{}) {
var _ca []interface{}
_ca = append(_ca, format)
_ca = append(_ca, args...)
_m.Called(_ca...)
}
// MockInterface_Printf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Printf'
type MockInterface_Printf_Call struct {
*mock.Call
}
// Printf is a helper method to define mock.On call
// - format string
// - args ...interface{}
func (_e *MockInterface_Expecter) Printf(format interface{}, args ...interface{}) *MockInterface_Printf_Call {
return &MockInterface_Printf_Call{Call: _e.mock.On("Printf",
append([]interface{}{format}, args...)...)}
}
func (_c *MockInterface_Printf_Call) Run(run func(format string, args ...interface{})) *MockInterface_Printf_Call {
_c.Call.Run(func(args mock.Arguments) {
variadicArgs := make([]interface{}, len(args)-1)
for i, a := range args[1:] {
if a != nil {
variadicArgs[i] = a.(interface{})
}
}
run(args[0].(string), variadicArgs...)
})
return _c
}
func (_c *MockInterface_Printf_Call) Return() *MockInterface_Printf_Call {
_c.Call.Return()
return _c
}
// V provides a mock function with given fields: level
func (_m *MockInterface) V(level int) Verbose {
ret := _m.Called(level)
var r0 Verbose
if rf, ok := ret.Get(0).(func(int) Verbose); ok {
r0 = rf(level)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(Verbose)
}
}
return r0
}
// MockInterface_V_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'V'
type MockInterface_V_Call struct {
*mock.Call
}
// V is a helper method to define mock.On call
// - level int
func (_e *MockInterface_Expecter) V(level interface{}) *MockInterface_V_Call {
return &MockInterface_V_Call{Call: _e.mock.On("V", level)}
}
func (_c *MockInterface_V_Call) Run(run func(level int)) *MockInterface_V_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(int))
})
return _c
}
func (_c *MockInterface_V_Call) Return(_a0 Verbose) *MockInterface_V_Call {
_c.Call.Return(_a0)
return _c
}
type mockConstructorTestingTNewMockInterface interface {
mock.TestingT
Cleanup(func())
}
// NewMockInterface creates a new instance of MockInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockInterface(t mockConstructorTestingTNewMockInterface) *MockInterface {
mock := &MockInterface{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

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