Merge pull request #165 from mattmoyer/cli-session-cache

Add basic file-based session cache for CLI OIDC client.
This commit is contained in:
Matt Moyer
2020-10-21 16:30:03 -05:00
committed by GitHub
15 changed files with 1377 additions and 53 deletions
+9
View File
@@ -13,3 +13,12 @@ func mustMarkRequired(cmd *cobra.Command, flags ...string) {
}
}
}
// mustMarkHidden marks the given flags as hidden on the provided cobra.Command. If any of the names are wrong, it panics.
func mustMarkHidden(cmd *cobra.Command, flags ...string) {
for _, flag := range flags {
if err := cmd.Flags().MarkHidden(flag); err != nil {
panic(err)
}
}
}
+55 -15
View File
@@ -5,20 +5,24 @@ package cmd
import (
"encoding/json"
"os"
"path/filepath"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/klog/v2/klogr"
"go.pinniped.dev/internal/oidcclient/login"
"go.pinniped.dev/internal/oidcclient"
"go.pinniped.dev/internal/oidcclient/filesession"
)
//nolint: gochecknoinits
func init() {
loginCmd.AddCommand(oidcLoginCommand(login.Run))
loginCmd.AddCommand(oidcLoginCommand(oidcclient.Login))
}
func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...login.Option) (*login.Token, error)) *cobra.Command {
func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...oidcclient.Option) (*oidcclient.Token, error)) *cobra.Command {
var (
cmd = cobra.Command{
Args: cobra.NoArgs,
@@ -26,32 +30,51 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...log
Short: "Login using an OpenID Connect provider",
SilenceUsage: true,
}
issuer string
clientID string
listenPort uint16
scopes []string
skipBrowser bool
issuer string
clientID string
listenPort uint16
scopes []string
skipBrowser bool
sessionCachePath string
debugSessionCache bool
)
cmd.Flags().StringVar(&issuer, "issuer", "", "OpenID Connect issuer URL.")
cmd.Flags().StringVar(&clientID, "client-id", "", "OpenID Connect client ID.")
cmd.Flags().Uint16Var(&listenPort, "listen-port", 0, "TCP port for localhost listener (authorization code flow only).")
cmd.Flags().StringSliceVar(&scopes, "scopes", []string{"offline_access", "openid", "email", "profile"}, "OIDC scopes to request during login.")
cmd.Flags().BoolVar(&skipBrowser, "skip-browser", false, "Skip opening the browser (just print the URL).")
cmd.Flags().StringVar(&sessionCachePath, "session-cache", filepath.Join(mustGetConfigDir(), "sessions.yaml"), "Path to session cache file.")
cmd.Flags().BoolVar(&debugSessionCache, "debug-session-cache", false, "Print debug logs related to the session cache.")
mustMarkHidden(&cmd, "debug-session-cache")
mustMarkRequired(&cmd, "issuer", "client-id")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
opts := []login.Option{
login.WithContext(cmd.Context()),
login.WithScopes(scopes),
// Initialize the session cache.
var sessionOptions []filesession.Option
// If the hidden --debug-session-cache option is passed, log all the errors from the session cache with klog.
if debugSessionCache {
logger := klogr.New().WithName("session")
sessionOptions = append(sessionOptions, filesession.WithErrorReporter(func(err error) {
logger.Error(err, "error during session cache operation")
}))
}
sessionCache := filesession.New(sessionCachePath, sessionOptions...)
// Initialize the login handler.
opts := []oidcclient.Option{
oidcclient.WithContext(cmd.Context()),
oidcclient.WithScopes(scopes),
oidcclient.WithSessionCache(sessionCache),
}
if listenPort != 0 {
opts = append(opts, login.WithListenPort(listenPort))
opts = append(opts, oidcclient.WithListenPort(listenPort))
}
// --skip-browser replaces the default "browser open" function with one that prints to stderr.
if skipBrowser {
opts = append(opts, login.WithBrowserOpen(func(url string) error {
opts = append(opts, oidcclient.WithBrowserOpen(func(url string) error {
cmd.PrintErr("Please log in: ", url, "\n")
return nil
}))
@@ -69,10 +92,27 @@ func oidcLoginCommand(loginFunc func(issuer string, clientID string, opts ...log
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
ExpirationTimestamp: &metav1.Time{Time: tok.IDTokenExpiry},
Token: tok.IDToken,
ExpirationTimestamp: &tok.IDToken.Expiry,
Token: tok.IDToken.Token,
},
})
}
return &cmd
}
// mustGetConfigDir returns a directory that follows the XDG base directory convention:
// $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should
// be stored. If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used.
// [1] https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
func mustGetConfigDir() string {
const xdgAppName = "pinniped"
if path := os.Getenv("XDG_CONFIG_HOME"); path != "" {
return filepath.Join(path, xdgAppName)
}
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
return filepath.Join(home, ".config", xdgAppName)
}
+22 -12
View File
@@ -9,14 +9,17 @@ import (
"time"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/oidcclient/login"
"go.pinniped.dev/internal/oidcclient"
)
func TestLoginOIDCCommand(t *testing.T) {
t.Parallel()
cfgDir := mustGetConfigDir()
time1 := time.Date(3020, 10, 12, 13, 14, 15, 16, time.UTC)
tests := []struct {
@@ -39,12 +42,13 @@ func TestLoginOIDCCommand(t *testing.T) {
oidc --issuer ISSUER --client-id CLIENT_ID [flags]
Flags:
--client-id string OpenID Connect client ID.
-h, --help help for oidc
--issuer string OpenID Connect issuer URL.
--listen-port uint16 TCP port for localhost listener (authorization code flow only).
--scopes strings OIDC scopes to request during login. (default [offline_access,openid,email,profile])
--skip-browser Skip opening the browser (just print the URL).
--client-id string OpenID Connect client ID.
-h, --help help for oidc
--issuer string OpenID Connect issuer URL.
--listen-port uint16 TCP port for localhost listener (authorization code flow only).
--scopes strings OIDC scopes to request during login. (default [offline_access,openid,email,profile])
--session-cache string Path to session cache file. (default "` + cfgDir + `/sessions.yaml")
--skip-browser Skip opening the browser (just print the URL).
`),
},
{
@@ -63,7 +67,7 @@ func TestLoginOIDCCommand(t *testing.T) {
},
wantIssuer: "test-issuer",
wantClientID: "test-client-id",
wantOptionsCount: 2,
wantOptionsCount: 3,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
{
@@ -73,10 +77,11 @@ func TestLoginOIDCCommand(t *testing.T) {
"--issuer", "test-issuer",
"--skip-browser",
"--listen-port", "1234",
"--debug-session-cache",
},
wantIssuer: "test-issuer",
wantClientID: "test-client-id",
wantOptionsCount: 4,
wantOptionsCount: 5,
wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n",
},
}
@@ -87,13 +92,18 @@ func TestLoginOIDCCommand(t *testing.T) {
var (
gotIssuer string
gotClientID string
gotOptions []login.Option
gotOptions []oidcclient.Option
)
cmd := oidcLoginCommand(func(issuer string, clientID string, opts ...login.Option) (*login.Token, error) {
cmd := oidcLoginCommand(func(issuer string, clientID string, opts ...oidcclient.Option) (*oidcclient.Token, error) {
gotIssuer = issuer
gotClientID = clientID
gotOptions = opts
return &login.Token{IDToken: "test-id-token", IDTokenExpiry: time1}, nil
return &oidcclient.Token{
IDToken: &oidcclient.IDToken{
Token: "test-id-token",
Expiry: metav1.NewTime(time1),
},
}, nil
})
require.NotNil(t, cmd)