// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "context" "encoding/json" "fmt" "io" "os" "path/filepath" "time" "github.com/spf13/cobra" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" "go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/oidcclient/oidctypes" ) //nolint: gochecknoinits func init() { loginCmd.AddCommand(staticLoginCommand(staticLoginRealDeps())) } type staticLoginDeps struct { lookupEnv func(string) (string, bool) exchangeToken func(context.Context, *conciergeclient.Client, string) (*clientauthv1beta1.ExecCredential, error) } func staticLoginRealDeps() staticLoginDeps { return staticLoginDeps{ lookupEnv: os.LookupEnv, exchangeToken: func(ctx context.Context, client *conciergeclient.Client, token string) (*clientauthv1beta1.ExecCredential, error) { return client.ExchangeToken(ctx, token) }, } } type staticLoginParams struct { staticToken string staticTokenEnvName string conciergeEnabled bool conciergeAuthenticatorType string conciergeAuthenticatorName string conciergeEndpoint string conciergeCABundle string conciergeAPIGroupSuffix string credentialCachePath string } func staticLoginCommand(deps staticLoginDeps) *cobra.Command { var ( cmd = &cobra.Command{ Args: cobra.NoArgs, Use: "static [--token TOKEN] [--token-env TOKEN_NAME]", Short: "Login using a static token", SilenceUsage: true, } flags staticLoginParams conciergeNamespace string // unused now ) cmd.Flags().StringVar(&flags.staticToken, "token", "", "Static token to present during login") cmd.Flags().StringVar(&flags.staticTokenEnvName, "token-env", "", "Environment variable containing a static token") cmd.Flags().BoolVar(&flags.conciergeEnabled, "enable-concierge", false, "Use the Concierge to login") cmd.Flags().StringVar(&conciergeNamespace, "concierge-namespace", "pinniped-concierge", "Namespace in which the Concierge was installed") cmd.Flags().StringVar(&flags.conciergeAuthenticatorType, "concierge-authenticator-type", "", "Concierge authenticator type (e.g., 'webhook', 'jwt')") cmd.Flags().StringVar(&flags.conciergeAuthenticatorName, "concierge-authenticator-name", "", "Concierge authenticator name") cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore") mustMarkHidden(cmd, "concierge-namespace") return cmd } func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams) error { if flags.staticToken == "" && flags.staticTokenEnvName == "" { return fmt.Errorf("one of --token or --token-env must be set") } var concierge *conciergeclient.Client if flags.conciergeEnabled { var err error concierge, err = conciergeclient.New( conciergeclient.WithEndpoint(flags.conciergeEndpoint), conciergeclient.WithBase64CABundle(flags.conciergeCABundle), conciergeclient.WithAuthenticator(flags.conciergeAuthenticatorType, flags.conciergeAuthenticatorName), conciergeclient.WithAPIGroupSuffix(flags.conciergeAPIGroupSuffix), ) if err != nil { return fmt.Errorf("invalid Concierge parameters: %w", err) } } var token string if flags.staticToken != "" { token = flags.staticToken } if flags.staticTokenEnvName != "" { var ok bool token, ok = deps.lookupEnv(flags.staticTokenEnvName) if !ok { return fmt.Errorf("--token-env variable %q is not set", flags.staticTokenEnvName) } if token == "" { return fmt.Errorf("--token-env variable %q is empty", flags.staticTokenEnvName) } } cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}}) // Look up cached credentials based on a hash of all the CLI arguments, the current token value, and the cluster info. cacheKey := struct { Args []string `json:"args"` Token string `json:"token"` ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"` }{ Args: os.Args[1:], Token: token, ClusterInfo: loadClusterInfo(), } var credCache *execcredcache.Cache if flags.credentialCachePath != "" { credCache = execcredcache.New(flags.credentialCachePath) if cred := credCache.Get(cacheKey); cred != nil { return json.NewEncoder(out).Encode(cred) } } // If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential. if concierge != nil { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var err error cred, err = deps.exchangeToken(ctx, concierge, token) if err != nil { return fmt.Errorf("could not complete Concierge credential exchange: %w", err) } } // If there was a credential cache, save the resulting credential for future use. We only save to the cache if // the credential came from the concierge, since that's the only static token case where the cache is useful. if credCache != nil && concierge != nil { credCache.Put(cacheKey, cred) } return json.NewEncoder(out).Encode(cred) }