diff --git a/README.md b/README.md index 89409ba..ab8ab58 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # go-passbolt-cli A CLI tool to interact with Passbolt, an Open source Password Manager for teams. -If you want to do something more complicated: [this Go Module](https://github.com/passbolt/go-passbolt) to Interact with Passbolt from Go might intrest you. +If you want to do something more complicated: [this Go Module](https://github.com/passbolt/go-passbolt) to Interact with Passbolt from Go might interest you. -Disclaimer: This project is community driven and not associated with Passbolt SA +Disclaimer: This project is community-driven and not associated with Passbolt SA # Install ## Via Repository (Prefered): @@ -28,7 +28,7 @@ Note: tab completion and manpages will need to be installed manually. Note: this will install the binary as go-passbolt-cli, also tab completion and manpages will be missing. # Getting Started -First you need to Setup basic information: the Server Address, your Private Key and your Password. +First, you need to set up basic information: the Server Address, your Private Key, and your Password. You have these options: - Save it in the config file using ``` @@ -43,23 +43,23 @@ passbolt configure --serverAddress https://passbolt.example.org --userPassword ' Notes: - You can set the Private Key using the flags `--userPrivateKey` or `--userPrivateKeyFile` where `--userPrivateKey` takes the actual private key and `--userPrivateKeyFile` loads the content of a file as the PrivateKey, `--userPrivateKeyFile` overwrites the value of `--userPrivateKey`. -- You can also just store the serverAddress and your Private Key, if your Password is not set it will prompt you for it every time. +- You can also just store the serverAddress and your Private Key, if your Password is not set, it will prompt you for it every time. - Passwordless PrivateKeys are unsupported -- MFA settings can also be save permanently this ways +- MFA settings can also be saved permanently these ways # Usage -Generally the Structure of Commands is like this: +Generally, the Structure of Commands is like this: ```bash passbolt action entity [arguments] ``` -Action is the Action you want to perform like Creating, Updating or Deleting an Entity. +Action is the Action you want to perform, like Creating, Updating, or Deleting an Entity. Entity is a Resource(Password), Folder, User or Group that you want to apply an action to. -In Passbolt a Password is usually revert to as a Resource. +In Passbolt, a Password is usually referred to as a Resource. -To Create a Resource you can do this, it will return the ID of the newly created Resource: +To Create a Resource, you can do this. It will return the ID of the newly created Resource: ```bash passbolt create resource --name "Test Resource" --password "Strong Password" ``` @@ -68,10 +68,10 @@ You can then list all users: ```bash passbolt list user ``` -Note: you can adjust which columns should be listed using the flag `--column` or its short from `-c`, if you want multiple column then you need to specify this flag multiple times. +Note: you can adjust which columns should be listed using the flag `--column` or its short from `-c`, if you want multiple columns then you need to specify this flag multiple times. -For sharing we will need to know how we want to share, for that there are these Permission Types: +For sharing, we will need to know how we want to share. For that, there are these Permission Types: | Code | Meaning | | --- | --- | @@ -80,32 +80,32 @@ For sharing we will need to know how we want to share, for that there are these | `15` | "Owner" | | `-1` | Delete existing permission | -Now that we have a Resource ID, know the ID's of other Users and about know about Permission Types, we can share the Resource with them: +Now that we have a Resource ID, know the ID's of other Users and know about Permission Types, we can share the Resource with them: ```bash passbolt share resource --id id_of_resource_to_share --type type_of_permission --user id_of_user_to_share_with ``` -Note: you can supply the the users argument multiple times to share with multiple users +Note: you can supply the `user` argument multiple times to share with multiple users -For sharing with groups the `--group` argument exists. +For sharing with groups, the `--group` argument exists. # MFA -You can setup MFA also using the configuration sub command, only TOTP is supported, there are multiple modes for MFA: `none`, `interactive-totp` and `noninteractive-totp`. +You can also set up MFA using the configuration subcommand. Only TOTP is supported, and there are multiple modes for MFA: `none`, `interactive-totp`, and `noninteractive-totp`. | Mode | Description | | --- | --- | |`none`|just errors if challenged for MFA. |`interactive-totp` | prompts for interactive entry of TOTP Codes. -|`noninteractive-totp` | automatically generates TOTP Codes when challenged, it requires the `mfaTotpToken` flag to be set to your totp Secret, you can configure the behavior using the `mfaDelay`, `mfaRetrys` and `mfaTotpOffset` flags +|`noninteractive-totp` | automatically generates TOTP Codes when challenged. It requires the `mfaTotpToken` flag to be set to your totp Secret, you can configure the behavior using the `mfaDelay`, `mfaRetrys` and `mfaTotpOffset` flags # Server Verification -To enable Server Verification you need to run `passbolt verify` once, after that the server will always be verified if the same config is used +To enable Server Verification, you need to run `passbolt verify` once. After that, the server will always be verified if the same config is used # Scripting -For Scripting we have a -j or --json flag to convert the Output for the create, get and list commands to JSON for easier Parsing in Scripts. +For Scripting, we have a -j or --json flag to convert the Output for the create, get and list commands to JSON for easier Parsing in Scripts. -Note: The JSON Output does not cover Error Messages, you can detect Errors by checking if the Exitcode is not 0 +Note: The JSON Output does not cover Error Messages. You can detect Errors by checking if the Exitcode is not 0 # Documentation Usage for all Subcommands is [here](https://github.com/passbolt/go-passbolt-cli/wiki/passbolt). -And is also available via `man passbolt` +It is also available via `man passbolt` diff --git a/cmd/root.go b/cmd/root.go index b1274e1..212ad88 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "io/ioutil" "os" "path/filepath" "time" @@ -60,6 +59,12 @@ func init() { rootCmd.PersistentFlags().Uint("mfaRetrys", 3, "How often to retry TOTP Auth, only used in nointeractive modes") rootCmd.PersistentFlags().Duration("mfaDelay", time.Second*10, "Delay between MFA Attempts, only used in noninteractive modes") + rootCmd.PersistentFlags().Bool("tlsSkipVerify", false, "Allow servers with self-signed certificates") + rootCmd.PersistentFlags().String("tlsClientPrivateKeyFile", "", "Client private key path for mtls") + rootCmd.PersistentFlags().String("tlsClientCertFile", "", "Client certificate path for mtls") + rootCmd.PersistentFlags().String("tlsClientPrivateKey", "", "Client private key for mtls") + rootCmd.PersistentFlags().String("tlsClientCert", "", "Client certificate for mtls") + viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) viper.BindPFlag("timeout", rootCmd.PersistentFlags().Lookup("timeout")) viper.BindPFlag("serverAddress", rootCmd.PersistentFlags().Lookup("serverAddress")) @@ -72,6 +77,22 @@ func init() { viper.BindPFlag("mfaTotpOffset", rootCmd.PersistentFlags().Lookup("mfaTotpOffset")) viper.BindPFlag("mfaRetrys", rootCmd.PersistentFlags().Lookup("mfaRetrys")) viper.BindPFlag("mfaDelay", rootCmd.PersistentFlags().Lookup("mfaDelay")) + + viper.BindPFlag("tlsSkipVerify", rootCmd.PersistentFlags().Lookup("tlsSkipVerify")) + viper.BindPFlag("tlsClientCert", rootCmd.PersistentFlags().Lookup("tlsClientCert")) + viper.BindPFlag("tlsClientPrivateKey", rootCmd.PersistentFlags().Lookup("tlsClientPrivateKey")) +} + +func fileToContent(file, contentFlag string) { + if viper.GetBool("debug") { + fmt.Fprintln(os.Stderr, "Loading file:", file) + } + content, err := os.ReadFile(file) + if err != nil { + fmt.Fprintln(os.Stderr, "Error Loading File: ", err) + os.Exit(1) + } + viper.Set(contentFlag, string(content)) } // initConfig reads in config file and ENV variables if set. @@ -107,18 +128,26 @@ func initConfig() { // Read in Private Key from File if userprivatekeyfile is set userprivatekeyfile, err := rootCmd.PersistentFlags().GetString("userPrivateKeyFile") if err == nil && userprivatekeyfile != "" { - if viper.GetBool("debug") { - fmt.Fprintln(os.Stderr, "Loading Private Key from File:", userprivatekeyfile) - } - content, err := ioutil.ReadFile(userprivatekeyfile) - if err != nil { - fmt.Fprintln(os.Stderr, "Error Loading Private Key from File: ", err) - os.Exit(1) - } - viper.Set("userprivatekey", string(content)) + fileToContent(userprivatekeyfile, "userPrivateKey") } else if err != nil && viper.GetBool("debug") { fmt.Fprintln(os.Stderr, "Getting Private Key File Flag:", err) } + + // Read in Client Certificate Private Key from File if tlsClientPrivateKeyFile is set + tlsclientprivatekeyfile, err := rootCmd.PersistentFlags().GetString("tlsClientPrivateKeyFile") + if err == nil && tlsclientprivatekeyfile != "" { + fileToContent(tlsclientprivatekeyfile, "tlsClientPrivateKey") + } else if err != nil && viper.GetBool("debug") { + fmt.Fprintln(os.Stderr, "Getting Client Certificate Private key File Flag:", err) + } + + // Read in Client Certificate from File if tlsClientCertFile is set + tlsclientcertfile, err := rootCmd.PersistentFlags().GetString("tlsClientCertFile") + if err == nil && tlsclientcertfile != "" { + fileToContent(tlsclientcertfile, "tlsClientCert") + } else if err != nil && viper.GetBool("debug") { + fmt.Fprintln(os.Stderr, "Getting Client Certificate File Flag:", err) + } } func SetVersionInfo(version, commit, date string, dirty bool) { diff --git a/cmd/verify.go b/cmd/verify.go index 0a12c7e..e11d41a 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -41,7 +41,11 @@ var verifyCMD = &cobra.Command{ fmt.Println() } - client, err := api.NewClient(nil, "", serverAddress, userPrivateKey, userPassword) + httpClient, err := util.GetHttpClient() + if err != nil { + return err + } + client, err := api.NewClient(httpClient, "", serverAddress, userPrivateKey, userPassword) if err != nil { return fmt.Errorf("Creating Client: %w", err) } diff --git a/util/client.go b/util/client.go index 194e500..060f985 100644 --- a/util/client.go +++ b/util/client.go @@ -65,7 +65,11 @@ func GetClient(ctx context.Context) (*api.Client, error) { fmt.Println() } - client, err := api.NewClient(nil, "", serverAddress, userPrivateKey, userPassword) + httpClient, err := GetHttpClient() + if err != nil { + return nil, err + } + client, err := api.NewClient(httpClient, "", serverAddress, userPrivateKey, userPassword) if err != nil { return nil, fmt.Errorf("Creating Client: %w", err) } diff --git a/util/http.go b/util/http.go new file mode 100644 index 0000000..f9943f9 --- /dev/null +++ b/util/http.go @@ -0,0 +1,44 @@ +package util + +import ( + "crypto/tls" + "fmt" + "net/http" + + "github.com/spf13/viper" +) + +func GetClientCertificate() (tls.Certificate, error) { + cert := viper.GetString("tlsClientCert") + certExists := cert != "" + key := viper.GetString("tlsClientPrivateKey") + keyExists := key != "" + if !certExists && !keyExists { + return tls.Certificate{}, nil + } + if certExists && !keyExists { + return tls.Certificate{}, fmt.Errorf("Client TLS private key is empty, but client TLS cert was set.") + } + if !certExists && keyExists { + return tls.Certificate{}, fmt.Errorf("Client TLS cert is empty, but client TLS private key was set.") + } + return tls.X509KeyPair([]byte(cert), []byte(key)) +} + +func GetHttpClient() (*http.Client, error) { + tlsSkipVerify := viper.GetBool("tlsSkipVerify") + cert, err := GetClientCertificate() + if err != nil { + return nil, err + } + httpClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: tlsSkipVerify, + }, + }, + } + + return &httpClient, nil +}