Proof of concept: client-side logout
Signed-off-by: Margo Crawford <margaretc@vmware.com>
This commit is contained in:
parent
89e68489ea
commit
8a4bbbfcbe
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
@ -125,7 +125,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen
|
func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLoginFlags) error { //nolint:funlen
|
||||||
pLogger, err := SetLogLevel(deps.lookupEnv)
|
pLogger, err := SetLogLevel(deps.lookupEnv, "Pinniped login: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.WarningErr("Received error while setting log level", err)
|
plog.WarningErr("Received error while setting log level", err)
|
||||||
}
|
}
|
||||||
@ -326,7 +326,7 @@ func tokenCredential(token *oidctypes.Token) *clientauthv1beta1.ExecCredential {
|
|||||||
return &cred
|
return &cred
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetLogLevel(lookupEnv func(string) (string, bool)) (plog.Logger, error) {
|
func SetLogLevel(lookupEnv func(string) (string, bool), prefix string) (plog.Logger, error) {
|
||||||
debug, _ := lookupEnv("PINNIPED_DEBUG")
|
debug, _ := lookupEnv("PINNIPED_DEBUG")
|
||||||
if debug == "true" {
|
if debug == "true" {
|
||||||
err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug)
|
err := plog.ValidateAndSetLogLevelGlobally(plog.LevelDebug)
|
||||||
@ -334,7 +334,7 @@ func SetLogLevel(lookupEnv func(string) (string, bool)) (plog.Logger, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger := plog.New("Pinniped login: ")
|
logger := plog.New(prefix)
|
||||||
return logger, nil
|
return logger, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
@ -84,7 +84,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
|
func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error {
|
||||||
pLogger, err := SetLogLevel(deps.lookupEnv)
|
pLogger, err := SetLogLevel(deps.lookupEnv, "Pinniped login: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
plog.WarningErr("Received error while setting log level", err)
|
plog.WarningErr("Received error while setting log level", err)
|
||||||
}
|
}
|
||||||
|
153
cmd/pinniped/cmd/logout.go
Normal file
153
cmd/pinniped/cmd/logout.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"go.pinniped.dev/internal/plog"
|
||||||
|
|
||||||
|
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"go.pinniped.dev/pkg/oidcclient"
|
||||||
|
"go.pinniped.dev/pkg/oidcclient/filesession"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoinits
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(newLogoutCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
type logoutFlags struct {
|
||||||
|
kubeconfigPath string
|
||||||
|
kubeconfigContextOverride string
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implements client side logout-- i.e. deleting the cached tokens and certificates for a user
|
||||||
|
// without telling the supervisor to forget about the users tokens. From a user experience
|
||||||
|
// perspective these are identical, but it leaves orphaned tokens lying around that the supervisor
|
||||||
|
// won't garbage collect for up to 9 hours.
|
||||||
|
// Fosite supports token revocation requests ala https://tools.ietf.org/html/rfc7009#section-2.1
|
||||||
|
// with their TokenRevocationHandler, but we would also want to turn around and revoke the upstream
|
||||||
|
// tokens in the case of OIDC.
|
||||||
|
// That's something that could be done to improve security and stop storage from getting too
|
||||||
|
// big.
|
||||||
|
// It works by parsing the provided kubeconfig to get the arguments to pinniped login oidc,
|
||||||
|
// grabbing the issuer and the cache paths, then using that issuer to find and delete the entry
|
||||||
|
// in the session cache.
|
||||||
|
func newLogoutCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Use: "logout",
|
||||||
|
Short: "Terminate the current user's session.",
|
||||||
|
}
|
||||||
|
flags := &logoutFlags{}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file")
|
||||||
|
cmd.Flags().StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)")
|
||||||
|
|
||||||
|
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
return runLogout(flags)
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogout(flags *logoutFlags) error {
|
||||||
|
pLogger, err := SetLogLevel(os.LookupEnv, "Pinniped logout: ")
|
||||||
|
if err != nil {
|
||||||
|
plog.WarningErr("Received error while setting log level", err)
|
||||||
|
}
|
||||||
|
clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride)
|
||||||
|
currentKubeConfig, err := clientConfig.RawConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// start by getting the current context or another context if provided.
|
||||||
|
contextName := currentKubeConfig.CurrentContext
|
||||||
|
if len(flags.kubeconfigContextOverride) > 0 {
|
||||||
|
contextName = flags.kubeconfigContextOverride
|
||||||
|
}
|
||||||
|
kubeContext, ok := currentKubeConfig.Contexts[contextName]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("couldn't find current context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// then get the authinfo associated with that context.
|
||||||
|
authInfo := currentKubeConfig.AuthInfos[kubeContext.AuthInfo]
|
||||||
|
if authInfo == nil {
|
||||||
|
return fmt.Errorf("could not find auth info-- are you sure this is a Pinniped kubeconfig?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the exec credential out of the authinfo and validate that it takes the shape of a pinniped login command.
|
||||||
|
exec := authInfo.Exec
|
||||||
|
if exec == nil {
|
||||||
|
return fmt.Errorf("could not find exec credential-- are you sure this is a Pinniped kubeconfig?")
|
||||||
|
}
|
||||||
|
execArgs := exec.Args
|
||||||
|
if execArgs == nil {
|
||||||
|
return fmt.Errorf("could not find exec credential arguments-- are you sure this is a Pinniped kubeconfig?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the arguments in the exec credential (which should be the pinniped login command).
|
||||||
|
loginCommand := oidcLoginCommand(oidcLoginCommandDeps{})
|
||||||
|
err = loginCommand.ParseFlags(execArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Get the issuer flag. If this doesn't exist we have no way to get in to the cache so we have to exit.
|
||||||
|
issuer := loginCommand.Flag("issuer").Value.String()
|
||||||
|
if issuer == "" {
|
||||||
|
return fmt.Errorf("could not find issuer-- are you sure this is a Pinniped kubeconfig?")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the session cache. If it doesn't exist just use the default value.
|
||||||
|
sessionCachePath := loginCommand.Flag("session-cache").Value.String()
|
||||||
|
if sessionCachePath == "" {
|
||||||
|
sessionCachePath = filepath.Join(mustGetConfigDir(), "sessions.yaml")
|
||||||
|
}
|
||||||
|
// Get the credential cache. If it doesn't exist just use the default value.
|
||||||
|
credentialCachePath := loginCommand.Flag("credential-cache").Value.String()
|
||||||
|
if credentialCachePath == "" {
|
||||||
|
credentialCachePath = filepath.Join(mustGetConfigDir(), "credentials.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this should probably be a more targeted removal rather than the whole file...
|
||||||
|
// but that involves figuring out the cache key which is hard.
|
||||||
|
// Remove the credential cache that stores the users x509 certificates.
|
||||||
|
err = os.Remove(credentialCachePath)
|
||||||
|
// a not found error is fine and we should move on and try to delete the
|
||||||
|
// session cache if possible. Other errors might be a problem.
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the cache entry for this issuer.
|
||||||
|
var sessionOptions []filesession.Option
|
||||||
|
sessionCache := filesession.New(sessionCachePath, sessionOptions...)
|
||||||
|
downstreamScopes := []string{coreosoidc.ScopeOfflineAccess, coreosoidc.ScopeOpenID, "pinniped:request-audience"}
|
||||||
|
sort.Strings(downstreamScopes)
|
||||||
|
sessionCacheKey := oidcclient.SessionCacheKey{
|
||||||
|
Issuer: issuer,
|
||||||
|
ClientID: "pinniped-cli",
|
||||||
|
Scopes: downstreamScopes,
|
||||||
|
RedirectURI: (&url.URL{Scheme: "http", Host: "localhost:0", Path: "/callback"}).String(),
|
||||||
|
}
|
||||||
|
deleted := sessionCache.DeleteToken(sessionCacheKey)
|
||||||
|
|
||||||
|
if deleted {
|
||||||
|
pLogger.Warning("Successfully logged out of session.")
|
||||||
|
} else {
|
||||||
|
// this is likely because you're already logged out, but you might still want to know.
|
||||||
|
pLogger.Warning("Could not find session to log out of.")
|
||||||
|
pLogger.Debug("debug info", "issuer", issuer, "session cache path", sessionCachePath, "credential cache path", credentialCachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -333,6 +333,7 @@ popd >/dev/null
|
|||||||
manifest=/tmp/pinniped-concierge.yaml
|
manifest=/tmp/pinniped-concierge.yaml
|
||||||
concierge_app_name="pinniped-concierge"
|
concierge_app_name="pinniped-concierge"
|
||||||
concierge_namespace="concierge"
|
concierge_namespace="concierge"
|
||||||
|
image_repo="$registry_repo"
|
||||||
webhook_url="https://local-user-authenticator.local-user-authenticator.svc/authenticate"
|
webhook_url="https://local-user-authenticator.local-user-authenticator.svc/authenticate"
|
||||||
webhook_ca_bundle="$(kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator -o 'jsonpath={.data.caCertificate}')"
|
webhook_ca_bundle="$(kubectl get secret local-user-authenticator-tls-serving-certificate --namespace local-user-authenticator -o 'jsonpath={.data.caCertificate}')"
|
||||||
discovery_url="$(TERM=dumb kubectl cluster-info | awk '/master|control plane/ {print $NF}')"
|
discovery_url="$(TERM=dumb kubectl cluster-info | awk '/master|control plane/ {print $NF}')"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package cachefile implements the file format for session caches.
|
// Package cachefile implements the file format for session caches.
|
||||||
@ -152,6 +152,19 @@ func (c *sessionCache) lookup(key oidcclient.SessionCacheKey) *sessionEntry {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete a cache entry by key. Returns whether it successfully deleted an entry.
|
||||||
|
func (c *sessionCache) delete(key oidcclient.SessionCacheKey) bool {
|
||||||
|
length := len(c.Sessions)
|
||||||
|
for i := range c.Sessions {
|
||||||
|
if reflect.DeepEqual(c.Sessions[i].Key, key) {
|
||||||
|
c.Sessions[i] = c.Sessions[length-1]
|
||||||
|
c.Sessions = c.Sessions[:length-1]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// insert a cache entry.
|
// insert a cache entry.
|
||||||
func (c *sessionCache) insert(entries ...sessionEntry) {
|
func (c *sessionCache) insert(entries ...sessionEntry) {
|
||||||
c.Sessions = append(c.Sessions, entries...)
|
c.Sessions = append(c.Sessions, entries...)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
// Package filesession implements a simple YAML file-based login.sessionCache.
|
// Package filesession implements a simple YAML file-based login.sessionCache.
|
||||||
@ -113,6 +113,22 @@ func (c *Cache) PutToken(key oidcclient.SessionCacheKey, token *oidctypes.Token)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteToken deletes the token from the session cache at the given cache cache key.
|
||||||
|
// It returns whether it deleted a token.
|
||||||
|
func (c *Cache) DeleteToken(key oidcclient.SessionCacheKey) bool {
|
||||||
|
_, err := os.Stat(c.path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// if the cache file doesn't exist there's no session info
|
||||||
|
// to delete
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
deleted := false
|
||||||
|
c.withCache(func(cache *sessionCache) {
|
||||||
|
deleted = cache.delete(key)
|
||||||
|
})
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
// withCache is an internal helper which locks, reads the cache, processes/mutates it with the provided function, then
|
// withCache is an internal helper which locks, reads the cache, processes/mutates it with the provided function, then
|
||||||
// saves it back to the file.
|
// saves it back to the file.
|
||||||
func (c *Cache) withCache(transact func(*sessionCache)) {
|
func (c *Cache) withCache(transact func(*sessionCache)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user