From c20dc1e81f9299da6f149f86bef1c2de63b92787 Mon Sep 17 00:00:00 2001 From: Jake Van Vorhis <83739412+jakedoublev@users.noreply.github.com> Date: Fri, 3 May 2024 11:22:18 -0400 Subject: [PATCH] feat(core): allow no-cache client credentials options throughout CLI (#142) The main goal of this is to: 1. cache on the user's OS by default 2. allow auth without caching when desired (for example Ubuntu has known [gnome keyring issues](https://github.com/zalando/go-keyring/issues/18)) 3. allow flexibility in how non-cached auth is passed into the CLI, knowing that `stdin` isn't always an open option for all authenticated commands 4. take inspiration from nice CLIs like [gcloud --cred-file](https://cloud.google.com/sdk/gcloud/reference/auth/login#:~:text=complete%20the%20authorization.-,%2D%2Dcred%2Dfile%3DCRED_FILE,-Path%20to%20the) and [gh --with-token](https://cli.github.com/manual/gh_auth_login#:~:text=browser%20to%20authenticate-,%2D%2Dwith%2Dtoken,-Read%20token%20from) 5. continue to authenticate to the platform via `sdk.WithClientCredentials` Demo: https://www.loom.com/share/96327c747ad94551a7487fc51fe31a7f?sid=6e2b81e1-7fe7-4f9f-8f34-0f6dd2a23ca6 --- cmd/auth-clearCachedCredentials.go | 1 + cmd/auth-clientCredentials.go | 57 +++++++++++++------ cmd/config.go | 2 +- cmd/dev-selectors.go | 4 +- cmd/dev.go | 21 +++++++ cmd/interactive.go | 3 +- cmd/kas-grants.go | 4 +- cmd/kas-registry.go | 10 ++-- cmd/policy-attributeNamespaces.go | 14 ++--- cmd/policy-attributeValues.go | 16 +++--- cmd/policy-attributes.go | 10 ++-- cmd/policy-resourceMappings.go | 10 ++-- cmd/policy-subjectConditionSets.go | 10 ++-- cmd/policy-subject_mappings.go | 10 ++-- cmd/root.go | 18 +++++- cmd/tdf-decrypt.go | 2 +- cmd/tdf-encrypt.go | 2 +- docs/man/_index.md | 7 ++- docs/man/auth/_index.md | 2 +- docs/man/auth/clear-cached-credentials.md | 4 +- docs/man/auth/client-credentials.md | 12 ++-- main.go | 2 +- pkg/cli/handlers.go | 21 ------- pkg/handlers/auth.go | 69 +++++++++++++++++++---- pkg/handlers/sdk.go | 8 +-- pkg/man/man.go | 13 +++++ 26 files changed, 218 insertions(+), 114 deletions(-) delete mode 100644 pkg/cli/handlers.go diff --git a/cmd/auth-clearCachedCredentials.go b/cmd/auth-clearCachedCredentials.go index cf271cb1..e7ef477a 100644 --- a/cmd/auth-clearCachedCredentials.go +++ b/cmd/auth-clearCachedCredentials.go @@ -13,6 +13,7 @@ import ( var clearCachedCredsCmd = man.Docs.GetCommand("auth/clear-cached-credentials", man.WithRun(auth_clearCreds), + man.WithHiddenFlags("with-client-creds", "with-client-creds-file"), ) func auth_clearCreds(cmd *cobra.Command, args []string) { diff --git a/cmd/auth-clientCredentials.go b/cmd/auth-clientCredentials.go index f106809d..c401c070 100644 --- a/cmd/auth-clientCredentials.go +++ b/cmd/auth-clientCredentials.go @@ -3,66 +3,91 @@ package cmd import ( "errors" "fmt" + "log/slog" "github.com/opentdf/otdfctl/pkg/cli" "github.com/opentdf/otdfctl/pkg/handlers" "github.com/opentdf/otdfctl/pkg/man" "github.com/spf13/cobra" - "github.com/zalando/go-keyring" ) -var clientCredentialsCmd = man.Docs.GetCommand("auth/client-credentials", - man.WithRun(auth_clientCredentials), +var ( + clientCredentialsCmd = man.Docs.GetCommand("auth/client-credentials", + man.WithRun(auth_clientCredentials), + ) + noCacheCreds bool ) func auth_clientCredentials(cmd *cobra.Command, args []string) { flagHelper := cli.NewFlagHelper(cmd) clientID := flagHelper.GetOptionalString("client-id") clientSecret := flagHelper.GetOptionalString("client-secret") + var err error + if clientCredsFile != "" { + creds, err := handlers.GetClientCredsFromFile(clientCredsFile) + if err != nil { + cli.ExitWithError("Failed to parse client credentials JSON", err) + } + clientID = creds.ClientID + clientSecret = creds.ClientSecret + } // if not provided by flag, check keyring cache for clientID if clientID == "" { - fmt.Println("No client-id provided. Attempting to retrieve the default from keyring.") - retrievedClientID, err := handlers.GetClientIDFromCache() - if err != nil || retrievedClientID == "" { + slog.Debug("No client-id provided. Attempting to retrieve the default from keyring.") + clientID, err = handlers.GetClientIDFromCache() + if err != nil || clientID == "" { cli.ExitWithError("Please provide required flag: (client-id)", errors.New("no client-id found")) } else { - clientID = retrievedClientID - fmt.Println(cli.SuccessMessage("Retrieved stored client-id from keyring")) + slog.Debug(cli.SuccessMessage("Retrieved stored client-id from keyring")) } } // check if we have a clientSecret in the keyring, if a null value is passed in if clientSecret == "" { - retrievedSecret, krErr := keyring.Get(handlers.TOKEN_URL, clientID) - if krErr == nil || retrievedSecret == "" { + clientSecret, err = handlers.GetClientSecretFromCache(clientID) + if err == nil || clientSecret == "" { cli.ExitWithError("Please provide required flag: (client-secret)", errors.New("no client-secret found")) } else { - clientSecret = retrievedSecret - fmt.Println(cli.SuccessMessage("Retrieved stored client-secret from keyring")) + slog.Debug("Retrieved stored client-secret from keyring") } } - _, err := handlers.GetTokenWithClientCredentials(cmd.Context(), clientID, clientSecret, handlers.TOKEN_URL, false) + tok, err := handlers.GetTokenWithClientCreds(cmd.Context(), clientID, clientSecret, handlers.TOKEN_URL, noCacheCreds) if err != nil { cli.ExitWithError("An error occurred during login. Please check your credentials and try again", err) } - fmt.Println(cli.SuccessMessage("Successfully logged in with client ID and secret")) + if !noCacheCreds { + fmt.Println(cli.SuccessMessage("Successfully logged in with client ID and secret")) + } else { + fmt.Print(tok.AccessToken) + } } func init() { clientCredentialsCmd := man.Docs.GetCommand("auth/client-credentials", man.WithRun(auth_clientCredentials), + // use the individual client-id and client-secret flags here instead of the global with-client-creds flag + man.WithHiddenFlags("with-client-creds"), ) - clientCredentialsCmd.Flags().String( + clientCredentialsCmd.Flags().StringP( clientCredentialsCmd.GetDocFlag("client-id").Name, + clientCredentialsCmd.GetDocFlag("client-id").Shorthand, clientCredentialsCmd.GetDocFlag("client-id").Default, clientCredentialsCmd.GetDocFlag("client-id").Description, ) - clientCredentialsCmd.Flags().String( + clientCredentialsCmd.Flags().StringP( clientCredentialsCmd.GetDocFlag("client-secret").Name, + clientCredentialsCmd.GetDocFlag("client-secret").Shorthand, clientCredentialsCmd.GetDocFlag("client-secret").Default, clientCredentialsCmd.GetDocFlag("client-secret").Description, ) + clientCredentialsCmd.Flags().BoolVarP( + &noCacheCreds, + clientCredentialsCmd.GetDocFlag("no-cache").Name, + clientCredentialsCmd.GetDocFlag("no-cache").Shorthand, + clientCredentialsCmd.GetDocFlag("no-cache").DefaultAsBool(), + clientCredentialsCmd.GetDocFlag("no-cache").Description, + ) } diff --git a/cmd/config.go b/cmd/config.go index caa0ac97..39a7d429 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -10,7 +10,7 @@ import ( ) func config_updateOutput(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/dev-selectors.go b/cmd/dev-selectors.go index 7036df02..952d2f80 100644 --- a/cmd/dev-selectors.go +++ b/cmd/dev-selectors.go @@ -19,7 +19,7 @@ var ( ) func dev_selectorsGen(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -62,7 +62,7 @@ func dev_selectorsGen(cmd *cobra.Command, args []string) { } func dev_selectorsTest(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/dev.go b/cmd/dev.go index 42ec876a..ff332683 100644 --- a/cmd/dev.go +++ b/cmd/dev.go @@ -3,6 +3,7 @@ package cmd import ( "bufio" "encoding/json" + "errors" "fmt" "io" "os" @@ -11,6 +12,7 @@ import ( "github.com/charmbracelet/lipgloss/table" "github.com/opentdf/otdfctl/internal/config" "github.com/opentdf/otdfctl/pkg/cli" + "github.com/opentdf/otdfctl/pkg/handlers" "github.com/opentdf/otdfctl/pkg/man" "github.com/opentdf/platform/protocol/go/common" "github.com/spf13/cobra" @@ -153,6 +155,25 @@ func readBytesFromFile(filePath string) []byte { return bytes } +// instantiates a new handler with authentication via client credentials +func NewHandler(cmd *cobra.Command) handlers.Handler { + platformEndpoint := cmd.Flag("host").Value.String() + + // load client credentials from file, JSON, or OS keyring + creds, err := handlers.GetClientCreds(clientCredsFile, []byte(clientCredsJSON)) + if err != nil { + cli.ExitWithError("Failed to get client credentials", err) + } + h, err := handlers.New(platformEndpoint, creds.ClientID, creds.ClientSecret) + if err != nil { + if errors.Is(err, handlers.ErrUnauthenticated) { + cli.ExitWithError(fmt.Sprintf("Not logged in. Please authenticate via CLI auth flow(s) before using command (%s %s)", cmd.Parent().Use, cmd.Use), err) + } + cli.ExitWithError("Failed to connect to server", err) + } + return h +} + func init() { designCmd := man.Docs.GetCommand("dev/design-system", man.WithRun(dev_designSystem), diff --git a/cmd/interactive.go b/cmd/interactive.go index df8689bf..c63d8636 100644 --- a/cmd/interactive.go +++ b/cmd/interactive.go @@ -1,7 +1,6 @@ package cmd import ( - "github.com/opentdf/otdfctl/pkg/cli" "github.com/opentdf/otdfctl/pkg/man" "github.com/opentdf/otdfctl/tui" "github.com/spf13/cobra" @@ -10,7 +9,7 @@ import ( func init() { cmd := man.Docs.GetCommand("interactive", man.WithRun(func(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) tui.StartTea(h) }), ) diff --git a/cmd/kas-grants.go b/cmd/kas-grants.go index 5c8a3e91..5585a16a 100644 --- a/cmd/kas-grants.go +++ b/cmd/kas-grants.go @@ -7,7 +7,7 @@ import ( ) func policy_updateKasGrant(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -51,7 +51,7 @@ func policy_updateKasGrant(cmd *cobra.Command, args []string) { } func policy_deleteKasGrant(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/kas-registry.go b/cmd/kas-registry.go index c8bfec6c..18d083f3 100644 --- a/cmd/kas-registry.go +++ b/cmd/kas-registry.go @@ -12,7 +12,7 @@ import ( var policy_kasRegistryCmd *cobra.Command func policy_getKeyAccessRegistry(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -43,7 +43,7 @@ func policy_getKeyAccessRegistry(cmd *cobra.Command, args []string) { } func policy_listKeyAccessRegistries(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() list, err := h.ListKasRegistryEntries() @@ -73,7 +73,7 @@ func policy_listKeyAccessRegistries(cmd *cobra.Command, args []string) { } func policy_createKeyAccessRegistry(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -122,7 +122,7 @@ func policy_createKeyAccessRegistry(cmd *cobra.Command, args []string) { } func policy_updateKeyAccessRegistry(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -168,7 +168,7 @@ func policy_updateKeyAccessRegistry(cmd *cobra.Command, args []string) { } func policy_deleteKeyAccessRegistry(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/policy-attributeNamespaces.go b/cmd/policy-attributeNamespaces.go index 39d87911..b86bc08f 100644 --- a/cmd/policy-attributeNamespaces.go +++ b/cmd/policy-attributeNamespaces.go @@ -11,12 +11,10 @@ import ( // TODO: add metadata to outputs once [https://github.com/opentdf/otdfctl/issues/73] is addressed -var ( - policy_attributeNamespacesCmd = man.Docs.GetCommand("policy/attributes/namespaces") -) +var policy_attributeNamespacesCmd = man.Docs.GetCommand("policy/attributes/namespaces") func policy_getAttributeNamespace(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -40,7 +38,7 @@ func policy_getAttributeNamespace(cmd *cobra.Command, args []string) { } func policy_listAttributeNamespaces(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() state := cli.GetState(cmd) @@ -65,7 +63,7 @@ func policy_listAttributeNamespaces(cmd *cobra.Command, args []string) { } func policy_createAttributeNamespace(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -89,7 +87,7 @@ func policy_createAttributeNamespace(cmd *cobra.Command, args []string) { } func policy_deactivateAttributeNamespace(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -121,7 +119,7 @@ func policy_deactivateAttributeNamespace(cmd *cobra.Command, args []string) { } func policy_updateAttributeNamespace(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/policy-attributeValues.go b/cmd/policy-attributeValues.go index 244ed8dd..c362fd29 100644 --- a/cmd/policy-attributeValues.go +++ b/cmd/policy-attributeValues.go @@ -22,7 +22,7 @@ func policy_createAttributeValue(cmd *cobra.Command, args []string) { metadataLabels := flagHelper.GetStringSlice("label", metadataLabels, cli.FlagHelperStringSliceOptions{Min: 0}) // TODO: support create with members when update is unblocked to remove/alter them after creation [https://github.com/opentdf/platform/issues/476] - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() attr, err := h.GetAttribute(attrId) @@ -42,7 +42,7 @@ func policy_getAttributeValue(cmd *cobra.Command, args []string) { flagHelper := cli.NewFlagHelper(cmd) id := flagHelper.GetRequiredString("id") - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() v, err := h.GetAttributeValue(id) @@ -54,7 +54,7 @@ func policy_getAttributeValue(cmd *cobra.Command, args []string) { } func policy_listAttributeValue(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) attrId := flagHelper.GetRequiredString("attribute-id") @@ -85,7 +85,7 @@ func policy_updateAttributeValue(cmd *cobra.Command, args []string) { id := flagHelper.GetRequiredString("id") metadataLabels := flagHelper.GetStringSlice("label", metadataLabels, cli.FlagHelperStringSliceOptions{Min: 0}) - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() _, err := h.GetAttributeValue(id) @@ -105,7 +105,7 @@ func policy_deactivateAttributeValue(cmd *cobra.Command, args []string) { flagHelper := cli.NewFlagHelper(cmd) id := flagHelper.GetRequiredString("id") - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() value, err := h.GetAttributeValue(id) @@ -144,7 +144,7 @@ func policy_deactivateAttributeValue(cmd *cobra.Command, args []string) { // id := flagHelper.GetRequiredString("id") // members := flagHelper.GetStringSlice("member", attrValueMembers, cli.FlagHelperStringSliceOptions{}) -// h := cli.NewHandler(cmd) +// h := NewHandler(cmd) // defer h.Close() // prev, err := h.GetAttributeValue(id) @@ -179,7 +179,7 @@ func policy_deactivateAttributeValue(cmd *cobra.Command, args []string) { // id := flagHelper.GetRequiredString("id") // members := flagHelper.GetStringSlice("members", attrValueMembers, cli.FlagHelperStringSliceOptions{}) -// h := cli.NewHandler(cmd) +// h := NewHandler(cmd) // defer h.Close() // prev, err := h.GetAttributeValue(id) @@ -223,7 +223,7 @@ func policy_deactivateAttributeValue(cmd *cobra.Command, args []string) { // id := flagHelper.GetRequiredString("id") // members := flagHelper.GetStringSlice("members", attrValueMembers, cli.FlagHelperStringSliceOptions{}) -// h := cli.NewHandler(cmd) +// h := NewHandler(cmd) // defer h.Close() // prev, err := h.GetAttributeValue(id) diff --git a/cmd/policy-attributes.go b/cmd/policy-attributes.go index bda1dbf3..8c372e2b 100644 --- a/cmd/policy-attributes.go +++ b/cmd/policy-attributes.go @@ -20,7 +20,7 @@ var ( ) func policy_createAttribute(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -61,7 +61,7 @@ func policy_getAttribute(cmd *cobra.Command, args []string) { flagHelper := cli.NewFlagHelper(cmd) id := flagHelper.GetRequiredString("id") - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() attr, err := h.GetAttribute(id) @@ -87,7 +87,7 @@ func policy_getAttribute(cmd *cobra.Command, args []string) { } func policy_listAttributes(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() state := cli.GetState(cmd) @@ -119,7 +119,7 @@ func policy_deactivateAttribute(cmd *cobra.Command, args []string) { flagHelper := cli.NewFlagHelper(cmd) id := flagHelper.GetRequiredString("id") - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() attr, err := h.GetAttribute(id) @@ -152,7 +152,7 @@ func policy_deactivateAttribute(cmd *cobra.Command, args []string) { } func policy_updateAttribute(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/policy-resourceMappings.go b/cmd/policy-resourceMappings.go index 123ed3bc..0322fab6 100644 --- a/cmd/policy-resourceMappings.go +++ b/cmd/policy-resourceMappings.go @@ -18,7 +18,7 @@ var ( ) func policy_createResourceMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -46,7 +46,7 @@ func policy_createResourceMapping(cmd *cobra.Command, args []string) { } func policy_getResourceMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -70,7 +70,7 @@ func policy_getResourceMapping(cmd *cobra.Command, args []string) { } func policy_listResourceMappings(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() rmList, err := h.ListResourceMappings() @@ -88,7 +88,7 @@ func policy_listResourceMappings(cmd *cobra.Command, args []string) { } func policy_updateResourceMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -115,7 +115,7 @@ func policy_updateResourceMapping(cmd *cobra.Command, args []string) { } func policy_deleteResourceMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/policy-subjectConditionSets.go b/cmd/policy-subjectConditionSets.go index f7f61b1e..d4a94098 100644 --- a/cmd/policy-subjectConditionSets.go +++ b/cmd/policy-subjectConditionSets.go @@ -13,7 +13,7 @@ import ( ) func policy_createSubjectConditionSet(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() var ( ss []*policy.SubjectSet @@ -77,7 +77,7 @@ func policy_createSubjectConditionSet(cmd *cobra.Command, args []string) { } func policy_getSubjectConditionSet(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -105,7 +105,7 @@ func policy_getSubjectConditionSet(cmd *cobra.Command, args []string) { } func policy_listSubjectConditionSets(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() scsList, err := h.ListSubjectConditionSets() @@ -129,7 +129,7 @@ func policy_listSubjectConditionSets(cmd *cobra.Command, args []string) { } func policy_updateSubjectConditionSet(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -173,7 +173,7 @@ func policy_updateSubjectConditionSet(cmd *cobra.Command, args []string) { } func policy_deleteSubjectConditionSet(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/policy-subject_mappings.go b/cmd/policy-subject_mappings.go index db68b0f7..0556b45d 100644 --- a/cmd/policy-subject_mappings.go +++ b/cmd/policy-subject_mappings.go @@ -20,7 +20,7 @@ var ( ) func policy_getSubjectMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -58,7 +58,7 @@ func policy_getSubjectMapping(cmd *cobra.Command, args []string) { } func policy_listSubjectMappings(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() list, err := h.ListSubjectMappings() @@ -96,7 +96,7 @@ func policy_listSubjectMappings(cmd *cobra.Command, args []string) { } func policy_createSubjectMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -169,7 +169,7 @@ func policy_createSubjectMapping(cmd *cobra.Command, args []string) { } func policy_deleteSubjectMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) @@ -197,7 +197,7 @@ func policy_deleteSubjectMapping(cmd *cobra.Command, args []string) { } func policy_updateSubjectMapping(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/root.go b/cmd/root.go index 6e55ecf8..0306ac42 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,8 +13,10 @@ import ( ) var ( - cfgKey string - OtdfctlCfg config.Config + cfgKey string + OtdfctlCfg config.Config + clientCredsFile string + clientCredsJSON string configFlagOverrides = config.ConfigFlagOverrides{} ) @@ -37,6 +39,18 @@ func init() { doc.GetDocFlag("log-level").Default, doc.GetDocFlag("log-level").Description, ) + RootCmd.PersistentFlags().StringVar( + &clientCredsFile, + doc.GetDocFlag("with-client-creds-file").Name, + doc.GetDocFlag("with-client-creds-file").Default, + doc.GetDocFlag("with-client-creds-file").Description, + ) + RootCmd.PersistentFlags().StringVar( + &clientCredsJSON, + doc.GetDocFlag("with-client-creds").Name, + doc.GetDocFlag("with-client-creds").Default, + doc.GetDocFlag("with-client-creds").Description, + ) RootCmd.AddGroup(&cobra.Group{ID: "tdf"}) } diff --git a/cmd/tdf-decrypt.go b/cmd/tdf-decrypt.go index f5ddb8c4..43839063 100644 --- a/cmd/tdf-decrypt.go +++ b/cmd/tdf-decrypt.go @@ -11,7 +11,7 @@ import ( ) func dev_tdfDecryptCmd(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/cmd/tdf-encrypt.go b/cmd/tdf-encrypt.go index 80ef87fc..e68061db 100644 --- a/cmd/tdf-encrypt.go +++ b/cmd/tdf-encrypt.go @@ -12,7 +12,7 @@ import ( ) func dev_tdfEncryptCmd(cmd *cobra.Command, args []string) { - h := cli.NewHandler(cmd) + h := NewHandler(cmd) defer h.Close() flagHelper := cli.NewFlagHelper(cmd) diff --git a/docs/man/_index.md b/docs/man/_index.md index 4c118632..d55409ed 100644 --- a/docs/man/_index.md +++ b/docs/man/_index.md @@ -6,7 +6,7 @@ command: aliases: [] flags: - name: host - description: host:port of the Virtru Data Security Platform gRPC server + description: host:port of the OpenTDF Platform gRPC server default: localhost:8080 - name: log-level description: log level @@ -18,4 +18,9 @@ command: - fatal - panic default: info + - name: with-client-creds-file + description: path to a JSON file containing a 'clientId' and 'clientSecret' for auth via client-credentials flow + - name: with-client-creds + description: JSON string containing a 'clientId' and 'clientSecret' for auth via client-credentials flow + default: '' --- diff --git a/docs/man/auth/_index.md b/docs/man/auth/_index.md index fd316cef..858ee028 100644 --- a/docs/man/auth/_index.md +++ b/docs/man/auth/_index.md @@ -5,4 +5,4 @@ command: name: auth --- -his command will allow you to manage your local authentication session in regards to the DSP platform. \ No newline at end of file +This command will allow you to manage your local authentication session with the OpenTDF platform. \ No newline at end of file diff --git a/docs/man/auth/clear-cached-credentials.md b/docs/man/auth/clear-cached-credentials.md index c4ad1e5a..5da5fb31 100644 --- a/docs/man/auth/clear-cached-credentials.md +++ b/docs/man/auth/clear-cached-credentials.md @@ -1,8 +1,8 @@ --- -title: Clear any cached OIDC token and client ID/Secret granted through the client credentials flow +title: Clear any cached OIDC access token, clientId, and clientSecret granted through the client credentials flow command: name: clear-cached-credentials --- -Clear any cached OIDC token and client ID/Secret on the native OS granted through the client credentials flow. +Clear any cached OIDC access token, `clientId`, and `clientSecret` on the native OS granted through the client credentials flow. diff --git a/docs/man/auth/client-credentials.md b/docs/man/auth/client-credentials.md index 92ae61e7..4fdc6709 100644 --- a/docs/man/auth/client-credentials.md +++ b/docs/man/auth/client-credentials.md @@ -5,11 +5,15 @@ command: name: client-credentials flags: - name: client-id - description: Client ID + description: A clientId for use in client-credentials auth flow + shorthand: i required: true - name: client-secret - description: Client secret + description: A clientSecret for use in client-credentials auth flow + shorthand: s + - name: no-cache + description: Do not cache credentials on the native OS and print access token to stdout instead --- -Allows the user to login in via client ID and secret. This will subsequently be stored in the -OS-specific keychain by default. +Allows the user to login in via Client ID and Secret. The client credentials and OIDC Access Token will be stored +in the OS-specific keychain by default, otherwise printed to `stdout` if `--no-cache` is passed. diff --git a/main.go b/main.go index 9d8bb9e7..619fe48c 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ func main() { // defer pprof.StopCPUProfile() l := new(slog.LevelVar) - l.Set(slog.LevelDebug) + l.Set(slog.LevelInfo) logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: l, })) diff --git a/pkg/cli/handlers.go b/pkg/cli/handlers.go deleted file mode 100644 index 14076592..00000000 --- a/pkg/cli/handlers.go +++ /dev/null @@ -1,21 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - - "github.com/opentdf/otdfctl/pkg/handlers" - "github.com/spf13/cobra" -) - -func NewHandler(cmd *cobra.Command) handlers.Handler { - platformEndpoint := cmd.Flag("host").Value.String() - h, err := handlers.New(platformEndpoint) - if err != nil { - if errors.Is(err, handlers.ErrUnauthenticated) { - ExitWithError(fmt.Sprintf("Not logged in. Please authenticate via CLI auth flow(s) before using command (%s)", cmd.Use), err) - } - ExitWithError("Failed to connect to server", err) - } - return h -} diff --git a/pkg/handlers/auth.go b/pkg/handlers/auth.go index 6bdc1f01..aec8a64d 100644 --- a/pkg/handlers/auth.go +++ b/pkg/handlers/auth.go @@ -2,7 +2,10 @@ package handlers import ( "context" + "encoding/json" + "errors" "fmt" + "os" "time" "github.com/golang-jwt/jwt/v4" @@ -54,23 +57,69 @@ func GetClientSecretFromCache(clientID string) (string, error) { return keyring.Get(TOKEN_URL, clientID) } -// GetClientSecretFromCache retrieves the client secret from the keyring. -func GetClientIdAndSecretFromCache() (string, string, error) { +// Client ID and Secret for use in the client credentials flow. +type ClientCreds struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` +} + +// Retrieves credentials by reading specified file +func GetClientCredsFromFile(filepath string) (ClientCreds, error) { + creds := ClientCreds{} + f, err := os.Open(filepath) + if err != nil { + return creds, errors.Join(errors.New("failed to open creds file"), err) + } + defer f.Close() + + if err := json.NewDecoder(f).Decode(&creds); err != nil { + return creds, errors.Join(errors.New("failed to decode creds file"), err) + } + + return creds, nil +} + +// Parse the JSON and return the client ID and secret +func GetClientCredsFromJSON(credsJSON []byte) (ClientCreds, error) { + creds := ClientCreds{} + if err := json.Unmarshal(credsJSON, &creds); err != nil { + return creds, errors.Join(errors.New("failed to decode creds JSON"), err) + } + + return creds, nil +} + +// Retrieves the client secret from the keyring. +func GetClientCredsFromCache() (ClientCreds, error) { + creds := ClientCreds{} // we use the client id to cache the secret, so retrieve it first - clientId, err := keyring.Get(TOKEN_URL, OTDFCTL_CLIENT_ID_CACHE_KEY) - if err != nil || clientId == "" { - return "", "", ErrUnauthenticated + clientID, err := keyring.Get(TOKEN_URL, OTDFCTL_CLIENT_ID_CACHE_KEY) + if err != nil || clientID == "" { + return creds, errors.Join(errors.New("could not find clientID in OS keyring"), ErrUnauthenticated) } - clientSecret, err := keyring.Get(TOKEN_URL, clientId) + clientSecret, err := keyring.Get(TOKEN_URL, clientID) if err != nil { - return "", "", err + return creds, err + } + return ClientCreds{ + ClientID: clientID, + ClientSecret: clientSecret, + }, nil +} + +func GetClientCreds(file string, credsJSON []byte) (ClientCreds, error) { + if file != "" { + return GetClientCredsFromFile(file) + } + if len(credsJSON) > 0 { + return GetClientCredsFromJSON(credsJSON) } - return clientSecret, clientId, nil + return GetClientCredsFromCache() } -// GetTokenWithClientCredentials uses the OAuth2 client credentials flow to obtain a token. -func GetTokenWithClientCredentials(ctx context.Context, clientID, clientSecret, tokenURL string, noCache bool) (*oauth2.Token, error) { +// Uses the OAuth2 client credentials flow to obtain a token. +func GetTokenWithClientCreds(ctx context.Context, clientID, clientSecret, tokenURL string, noCache bool) (*oauth2.Token, error) { // did the user pass a custom tokenURL? if tokenURL == "" { // use the default hardcoded constant diff --git a/pkg/handlers/sdk.go b/pkg/handlers/sdk.go index 32ef8d12..7c40c993 100644 --- a/pkg/handlers/sdk.go +++ b/pkg/handlers/sdk.go @@ -22,14 +22,10 @@ type Handler struct { } // Creates a new handler wrapping the SDK, which is authenticated through the cached client-credentials flow tokens -func New(platformEndpoint string) (Handler, error) { +func New(platformEndpoint, clientID, clientSecret string) (Handler, error) { scopes := []string{"email"} - clientSecret, clientId, err := GetClientIdAndSecretFromCache() - if err != nil { - return Handler{}, err - } - sdk, err := sdk.New(platformEndpoint, sdk.WithClientCredentials(clientId, clientSecret, scopes), sdk.WithTokenEndpoint(TOKEN_URL), sdk.WithInsecureConn()) + sdk, err := sdk.New(platformEndpoint, sdk.WithClientCredentials(clientID, clientSecret, scopes), sdk.WithTokenEndpoint(TOKEN_URL), sdk.WithInsecureConn()) if err != nil { return Handler{}, err } diff --git a/pkg/man/man.go b/pkg/man/man.go index 8d4d0ef7..f6aebf14 100644 --- a/pkg/man/man.go +++ b/pkg/man/man.go @@ -53,6 +53,19 @@ func WithRun(f func(cmd *cobra.Command, args []string)) CommandOpts { } } +// Hide any global or persisent flags from parent commands on the given command +func WithHiddenFlags(flags ...string) CommandOpts { + return func(d *Doc) { + // to hide root global flags, must set a custom help func that hides then calls the parent help func + d.SetHelpFunc(func(command *cobra.Command, strings []string) { + for _, f := range flags { + command.Flags().MarkHidden(f) + } + command.Parent().HelpFunc()(command, strings) + }) + } +} + type Manual struct { lang string Docs map[string]*Doc