Add integration test

This commit is contained in:
Hidetake Iwata
2018-08-20 23:53:39 +09:00
parent b776bac764
commit 8a7da83338
9 changed files with 398 additions and 68 deletions

View File

@@ -10,6 +10,7 @@ jobs:
- run: go get github.com/golang/lint/golint
- run: golint
- run: go build -v
- run: go test -v ./...
release:
docker:

83
cli/cli.go Normal file
View File

@@ -0,0 +1,83 @@
package cli
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"github.com/int128/kubelogin/authn"
"github.com/int128/kubelogin/kubeconfig"
flags "github.com/jessevdk/go-flags"
homedir "github.com/mitchellh/go-homedir"
"golang.org/x/oauth2"
)
// Parse parses command line arguments and returns a CLI instance.
func Parse(args []string) (*CLI, error) {
var cli CLI
parser := flags.NewParser(&cli, flags.HelpFlag)
args, err := parser.Parse()
if err != nil {
return nil, err
}
if len(args) > 0 {
return nil, fmt.Errorf("Too many argument")
}
return &cli, nil
}
// CLI represents an interface of this command.
type CLI struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
// CertificateAuthority string `long:"certificate-authority" env:"KUBELOGIN_CERTIFICATE_AUTHORITY" description:"Path to a cert file for the certificate authority"`
}
// ExpandKubeConfig returns an expanded KubeConfig path.
func (c *CLI) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(c.KubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s", c.KubeConfig)
}
return d, nil
}
// Run performs this command.
func (c *CLI) Run() error {
path, err := c.ExpandKubeConfig()
if err != nil {
return err
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Load(path)
if err != nil {
return fmt.Errorf("Could not load kubeconfig: %s", err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
return fmt.Errorf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
if err != nil {
return fmt.Errorf("Could not find auth-provider: %s", err)
}
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}}
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
return fmt.Errorf("Authentication error: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
return nil
}

View File

@@ -0,0 +1,103 @@
package integration
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"html/template"
"log"
"math/big"
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
)
// AuthHandler provides the stub handler for OIDC authentication.
type AuthHandler struct {
// Values in templates
Issuer string
AuthCode string
IDToken string
PrivateKey struct{ N, E string }
// Response templates
discoveryJSON *template.Template
tokenJSON *template.Template
jwksJSON *template.Template
}
// NewAuthHandler returns a new AuthHandler.
func NewAuthHandler(issuer string) *AuthHandler {
h := &AuthHandler{
Issuer: issuer,
AuthCode: "0b70006b-f62a-4438-aba5-c0b96775d8e5",
discoveryJSON: template.Must(template.ParseFiles("testdata/oidc-discovery.json")),
tokenJSON: template.Must(template.ParseFiles("testdata/oidc-token.json")),
jwksJSON: template.Must(template.ParseFiles("testdata/oidc-jwks.json")),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.StandardClaims{
Issuer: h.Issuer,
Audience: "kubernetes",
ExpiresAt: time.Now().Add(time.Hour).Unix(),
})
k, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
log.Fatal(err)
}
h.IDToken, err = token.SignedString(k)
if err != nil {
log.Fatal(err)
}
h.PrivateKey.E = base64.RawURLEncoding.EncodeToString(big.NewInt(int64(k.E)).Bytes())
h.PrivateKey.N = base64.RawURLEncoding.EncodeToString(k.N.Bytes())
return h
}
func (s *AuthHandler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
m := r.Method
p := r.URL.Path
log.Printf("[auth-server] %s %s", m, r.RequestURI)
switch {
case m == "GET" && p == "/.well-known/openid-configuration":
w.Header().Add("Content-Type", "application/json")
if err := s.discoveryJSON.Execute(w, s); err != nil {
return err
}
case m == "GET" && p == "/protocol/openid-connect/auth":
// Authentication Response
// http://openid.net/specs/openid-connect-core-1_0.html#AuthResponse
q := r.URL.Query()
to := fmt.Sprintf("%s?state=%s&code=%s", q.Get("redirect_uri"), q.Get("state"), s.AuthCode)
http.Redirect(w, r, to, 302)
case m == "POST" && p == "/protocol/openid-connect/token":
// Token Response
// http://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
if err := r.ParseForm(); err != nil {
return err
}
if s.AuthCode != r.Form.Get("code") {
return fmt.Errorf("code wants %s but %s", s.AuthCode, r.Form.Get("code"))
}
w.Header().Add("Content-Type", "application/json")
if err := s.tokenJSON.Execute(w, s); err != nil {
return err
}
case m == "GET" && p == "/protocol/openid-connect/certs":
w.Header().Add("Content-Type", "application/json")
if err := s.jwksJSON.Execute(w, s); err != nil {
return err
}
default:
http.Error(w, "Not Found", 404)
}
return nil
}
func (s *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := s.serveHTTP(w, r); err != nil {
log.Printf("[auth-server] Error: %s", err)
w.WriteHeader(500)
}
}

View File

@@ -0,0 +1,81 @@
package integration
import (
"context"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"testing"
"text/template"
"time"
"github.com/int128/kubelogin/cli"
)
type configuration struct {
Issuer string
}
func Test(t *testing.T) {
conf := configuration{
Issuer: "http://localhost:9000",
}
authServer := &http.Server{
Addr: "localhost:9000",
Handler: NewAuthHandler(conf.Issuer),
}
defer authServer.Shutdown(context.Background())
kubeconfig := createKubeconfig(t, conf)
defer os.Remove(kubeconfig)
go func() {
if err := authServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Error(err)
}
}()
go func() {
time.Sleep(100 * time.Millisecond)
res, err := http.Get("http://localhost:8000/")
if err != nil {
t.Error(err)
}
if res.StatusCode != 200 {
t.Errorf("StatusCode wants 200 but %d: res=%+v", res.StatusCode, res)
}
}()
c := cli.CLI{KubeConfig: kubeconfig}
if err := c.Run(); err != nil {
t.Fatal(err)
}
b, err := ioutil.ReadFile(kubeconfig)
if err != nil {
t.Fatal(err)
}
if strings.Index(string(b), "id-token: ey") == -1 {
t.Errorf("kubeconfig wants id-token but %s", string(b))
}
if strings.Index(string(b), "refresh-token: 44df4c82-5ce7-4260-b54d-1da0d396ef2a") == -1 {
t.Errorf("kubeconfig wants refresh-token but %s", string(b))
}
}
func createKubeconfig(t *testing.T, conf configuration) string {
t.Helper()
f, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
t.Fatal(err)
}
defer f.Close()
tpl, err := template.ParseFiles("testdata/kubeconfig.yaml")
if err != nil {
t.Fatal(err)
}
if err := tpl.Execute(f, conf); err != nil {
t.Fatal(err)
}
log.Printf("Created %s", f.Name())
return f.Name()
}

View File

@@ -0,0 +1,22 @@
apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://api.hello.k8s.example.com
name: hello.k8s.local
contexts:
- context:
cluster: hello.k8s.local
user: hello.k8s.local
name: hello.k8s.local
current-context: hello.k8s.local
preferences: {}
users:
- name: hello.k8s.local
user:
auth-provider:
config:
client-id: kubernetes
client-secret: a3c508c3-73c9-42e2-ab14-487a1bf67c33
idp-issuer-url: {{ .Issuer }}
name: oidc

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
{
"access_token": "7eaae8ab-8f69-45d9-ab7c-73560cd9444d",
"token_type": "Bearer",
"refresh_token": "44df4c82-5ce7-4260-b54d-1da0d396ef2a",
"expires_in": 3600,
"id_token": "{{ .IDToken }}"
}

72
main.go
View File

@@ -1,82 +1,18 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"os"
"github.com/int128/kubelogin/authn"
"github.com/int128/kubelogin/kubeconfig"
flags "github.com/jessevdk/go-flags"
homedir "github.com/mitchellh/go-homedir"
"golang.org/x/oauth2"
"github.com/int128/kubelogin/cli"
)
type options struct {
KubeConfig string `long:"kubeconfig" default:"~/.kube/config" env:"KUBECONFIG" description:"Path to the kubeconfig file"`
SkipTLSVerify bool `long:"insecure-skip-tls-verify" env:"KUBELOGIN_INSECURE_SKIP_TLS_VERIFY" description:"If set, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure"`
// CertificateAuthority string `long:"certificate-authority" env:"KUBELOGIN_CERTIFICATE_AUTHORITY" description:"Path to a cert file for the certificate authority"`
}
func (o *options) ExpandKubeConfig() (string, error) {
d, err := homedir.Expand(o.KubeConfig)
if err != nil {
return "", fmt.Errorf("Could not expand %s", o.KubeConfig)
}
return d, nil
}
func parseOptions() (*options, error) {
var o options
parser := flags.NewParser(&o, flags.HelpFlag)
args, err := parser.Parse()
if err != nil {
return nil, err
}
if len(args) > 0 {
return nil, fmt.Errorf("Too many argument")
}
return &o, nil
}
func main() {
opts, err := parseOptions()
c, err := cli.Parse(os.Args)
if err != nil {
log.Fatal(err)
}
path, err := opts.ExpandKubeConfig()
if err != nil {
if err := c.Run(); err != nil {
log.Fatal(err)
}
log.Printf("Reading %s", path)
cfg, err := kubeconfig.Load(path)
if err != nil {
log.Fatalf("Could not load kubeconfig: %s", err)
}
log.Printf("Using current context: %s", cfg.CurrentContext)
authInfo := kubeconfig.FindCurrentAuthInfo(cfg)
if authInfo == nil {
log.Fatalf("Could not find current context: %s", cfg.CurrentContext)
}
authProvider, err := kubeconfig.ToOIDCAuthProviderConfig(authInfo)
if err != nil {
log.Fatalf("Could not find auth-provider: %s", err)
}
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: opts.SkipTLSVerify},
}}
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
token, err := authn.GetTokenSet(ctx, authProvider.IDPIssuerURL(), authProvider.ClientID(), authProvider.ClientSecret())
if err != nil {
log.Fatalf("Authentication error: %s", err)
}
authProvider.SetIDToken(token.IDToken)
authProvider.SetRefreshToken(token.RefreshToken)
kubeconfig.Write(cfg, path)
log.Printf("Updated %s", path)
}