From 83c6e4a1738b7e1f6f87d860715961c80a7e2b57 Mon Sep 17 00:00:00 2001 From: Joe Garcia Date: Tue, 30 Mar 2021 10:50:12 -0400 Subject: [PATCH] v0.1.2-beta Release (#104) * #70 Add auto completion tab for fish, bash, zsh and powershell (#83) * Add auto completion tab for fish, bash, zsh and powershell * Update README to include instuctions for auto-complete * Add concurrent sessions to the CLI #88 (#89) * #96 allow more than 2 certificates in cert chain (#97) * Revert "#90 add no limit to default cybr conjur list command" (#102) * Add installation instruction for AWS CloudShell (#101) * #90 add no limit to default cybr conjur list command (#94) * Revert "#90 add no limit to default cybr conjur list command (#94)" This reverts commit 04d2f8cf1d39f432c2325212f1b9194c09c052b2. Co-authored-by: Quincy Cheng Co-authored-by: Andrew Copeland <50109276+AndrewCopeland@users.noreply.github.com> * V0.1.2 beta #47 account move (#93) * Change platform properties to map[string]string to me consistent with requests.AddAccount * #44 move an account to a new safe * Resolves #98 so windows login works correctly (#99) * Resolves #98 so windows login works correctly * Use strings.TrimSpace() instead of strings.Replace() * Add --password flag so CCP can retrieve REST API password #66 (#92) * Add --password flag to so CCP can retrieve REST API password #66 * Move check to beginning Co-authored-by: Andrew Copeland <50109276+AndrewCopeland@users.noreply.github.com> Co-authored-by: Quincy Cheng --- README.md | 20 ++++++++ cmd/accounts.go | 58 +++++++++++++++++++++++ cmd/completion.go | 70 ++++++++++++++++++++++++++++ cmd/logon.go | 21 +++++++-- pkg/cybr/api/responses/getaccount.go | 2 +- pkg/cybr/conjur/client.go | 6 --- pkg/cybr/conjur/conjurrc.go | 14 +++--- pkg/cybr/conjur/netrc.go | 12 +++-- 8 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 cmd/completion.go diff --git a/README.md b/README.md index 8299fcc..8cec8ed 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,25 @@ $ cybr help All commands are documentated [in the docs/ directory](docs/cybr.md). +## Autocomplete +The `cybr` CLI has a `completion` command that can be used to enable autocomplete for the CLI. +The completion command is dependant on your shell type. Currently the only shells that are supported are: bash, zsh, fish and powershell. + +Below is an example on how to enable `cybr` cli auto-completion from a zsh shell. +```bash +# enable shell completetion. Only needs to be performed once. +echo "autoload -U compinit; compinit" >> ~/.zshrc + +# create and write the auto-completion script. +# ${fpath[1]} '1' may be different depending on your environment. +cybr completion zsh > "${fpath[1]}/_cybr" +``` + +If you are using a different shell execute the `completion` command with the `--help` flag and follow instructions for the desired shell type. +```bash +cybr completion --help +``` + ## Example Source Code ### Logon to the PAS REST API Web Service @@ -94,6 +113,7 @@ func main() { log.Fatalf("Authentication failed. %s", errLogon) } fmt.Printf("Session Token:\r\n%s\r\n\r\n", token) +} ``` ## Testing diff --git a/cmd/accounts.go b/cmd/accounts.go index 0e87bfb..cb5d7a3 100644 --- a/cmd/accounts.go +++ b/cmd/accounts.go @@ -313,6 +313,57 @@ var reconcileAccountCmd = &cobra.Command{ }, } +var moveAccountCmd = &cobra.Command{ + Use: "move", + Short: "Move an account to a different safe", + Long: `Move an account to a different safe + + Example Usage: + $ cybr accounts move -i 24_1 -s newSafeName`, + Run: func(cmd *cobra.Command, args []string) { + client, err := pasapi.GetConfigWithLogger(getLogger()) + if err != nil { + log.Fatalf("Failed to read configuration file. %s", err) + return + } + + account, err := client.GetAccount(AccountID) + if err != nil { + log.Fatalf("%s", err) + } + + secret, err := client.GetAccountPassword(AccountID, requests.GetAccountPassword{}) + if err != nil { + log.Fatalf("%s", err) + } + + newAccount := requests.AddAccount{ + Name: account.Name, + Address: account.Address, + UserName: account.UserName, + PlatformID: account.PlatformID, + SafeName: Safe, + SecretType: account.SecretType, + Secret: secret, + PlatformAccountProperties: account.PlatformAccountProperties, + SecretManagement: account.SecretManagement, + } + + createdAccount, err := client.AddAccount(newAccount) + if err != nil { + log.Fatalf("%s", err) + } + + err = client.DeleteAccount(AccountID) + if err != nil { + log.Fatalf("%s", err) + return + } + + prettyprint.PrintJSON(createdAccount) + }, +} + func init() { // Listing an account listAccountsCmd.Flags().StringVarP(&Search, "search", "s", "", "List of keywords to search for in accounts, separated by a space") @@ -365,6 +416,12 @@ func init() { reconcileAccountCmd.Flags().StringVarP(&AccountID, "account-id", "i", "", "Account ID to reconcile") reconcileAccountCmd.MarkFlagRequired("account-id") + // move + moveAccountCmd.Flags().StringVarP(&AccountID, "account-id", "i", "", "Account ID to move") + moveAccountCmd.MarkFlagRequired("account-id") + moveAccountCmd.Flags().StringVarP(&Safe, "safe", "s", "", "Safe name in which the account will be moved into") + moveAccountCmd.MarkFlagRequired("safe") + // Add cmd to account cmd accountsCmd.AddCommand(listAccountsCmd) accountsCmd.AddCommand(getAccountsCmd) @@ -374,6 +431,7 @@ func init() { accountsCmd.AddCommand(verifyAccountCmd) accountsCmd.AddCommand(changeAccountCmd) accountsCmd.AddCommand(reconcileAccountCmd) + accountsCmd.AddCommand(moveAccountCmd) // Add accounts cmd to root rootCmd.AddCommand(accountsCmd) diff --git a/cmd/completion.go b/cmd/completion.go new file mode 100644 index 0000000..91adb57 --- /dev/null +++ b/cmd/completion.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: `To load completions: + +Bash: + + $ source <(cybr completion bash) + + # To load completions for each session, execute once: + # Linux: + $ cybr completion bash > /etc/bash_completion.d/cybr + # macOS: + $ cybr completion bash > /usr/local/etc/bash_completion.d/cybr + +Zsh: + + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ cybr completion zsh > "${fpath[1]}/_cybr" + + # You will need to start a new shell for this setup to take effect. + +fish: + + $ cybr completion fish | source + + # To load completions for each session, execute once: + $ cybr completion fish > ~/.config/fish/completions/cybr.fish + +PowerShell: + + PS> cybr completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> cybr completion powershell > cybr.ps1 + # and source this file from your PowerShell profile. +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.ExactValidArgs(1), + Run: func(cmd *cobra.Command, args []string) { + switch args[0] { + case "bash": + cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + cmd.Root().GenPowerShellCompletion(os.Stdout) + } + }, +} + +func init() { + rootCmd.AddCommand(completionCmd) +} diff --git a/cmd/logon.go b/cmd/logon.go index 17c093a..5446e99 100644 --- a/cmd/logon.go +++ b/cmd/logon.go @@ -24,6 +24,10 @@ var ( BaseURL string // NonInteractive logon NonInteractive bool + // Password to logon to the PAS REST API + Password string + // ConcurrentSession allow concurrent sessions + ConcurrentSession bool ) var logonCmd = &cobra.Command{ @@ -39,6 +43,10 @@ var logonCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { password := os.Getenv("PAS_PASSWORD") + if !NonInteractive && Password != "" { + log.Fatalf("An error occured because --non-interactive must be provided when using --password flag") + } + if !NonInteractive { // Get secret value from STDIN fmt.Print("Enter password: ") @@ -51,6 +59,10 @@ var logonCmd = &cobra.Command{ password = string(byteSecretVal) } + if Password != "" { + password = Password + } + if password == "" { log.Fatalf("Provided password is empty") } @@ -62,8 +74,9 @@ var logonCmd = &cobra.Command{ } credentials := requests.Logon{ - Username: Username, - Password: password, + Username: Username, + Password: password, + ConcurrentSession: ConcurrentSession, } err := client.Logon(credentials) @@ -99,7 +112,7 @@ var logonCmd = &cobra.Command{ } func init() { - logonCmd.Flags().StringVarP(&Username, "username", "u", "", "Username to logon PAS REST API using") + logonCmd.Flags().StringVarP(&Username, "username", "u", "", "Username to logon to PAS REST API") logonCmd.MarkFlagRequired("username") logonCmd.Flags().StringVarP(&AuthenticationType, "auth-type", "a", "", "Authentication method to logon using [cyberark|ldap|radius]") logonCmd.MarkFlagRequired("authType") @@ -107,6 +120,8 @@ func init() { logonCmd.Flags().StringVarP(&BaseURL, "base-url", "b", "", "Base URL to send Logon request to [https://pvwa.example.com]") logonCmd.MarkFlagRequired("base-url") logonCmd.Flags().BoolVar(&NonInteractive, "non-interactive", false, "If detected, will retrieve the password from the PAS_PASSWORD environment variable") + logonCmd.Flags().StringVarP(&Password, "password", "p", "", "Password to logon to PAS REST API, only supported when using --non-interactive flag") + logonCmd.Flags().BoolVar(&ConcurrentSession, "concurrent", false, "If detected, will create a concurrent session to the PAS API") rootCmd.AddCommand(logonCmd) } diff --git a/pkg/cybr/api/responses/getaccount.go b/pkg/cybr/api/responses/getaccount.go index 915d332..90a8e6a 100644 --- a/pkg/cybr/api/responses/getaccount.go +++ b/pkg/cybr/api/responses/getaccount.go @@ -12,7 +12,7 @@ type GetAccount struct { PlatformID string `json:"platformId"` SafeName string `json:"safeName"` SecretType string `json:"secretType"` - PlatformAccountProperties map[string]interface{} `json:"platformAccountProperties"` + PlatformAccountProperties map[string]string `json:"platformAccountProperties"` SecretManagement shared.SecretManagement `json:"secretManagement"` CreatedTime int `json:"createdTime"` } diff --git a/pkg/cybr/conjur/client.go b/pkg/cybr/conjur/client.go index 66350a6..b2666f0 100644 --- a/pkg/cybr/conjur/client.go +++ b/pkg/cybr/conjur/client.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "os" - "path/filepath" "strings" "github.com/cyberark/conjur-api-go/conjurapi" @@ -77,11 +76,6 @@ func getClientFromEnvironmentVariable() (*conjurapi.Client, *authn.LoginPair, er return client, &loginPair, err } -// GetNetRcPath returns path to the ~/.netrc file os-agnostic -func GetNetRcPath(homeDir string) string { - return filepath.FromSlash(fmt.Sprintf("%s/.netrc", homeDir)) -} - // GetConjurClient create conjur client and login pair for ~/.conjurrc and ~/.netrc func GetConjurClient() (*conjurapi.Client, *authn.LoginPair, error) { client, loginPair, err := getClientFromEnvironmentVariable() diff --git a/pkg/cybr/conjur/conjurrc.go b/pkg/cybr/conjur/conjurrc.go index e66793c..15ddb65 100644 --- a/pkg/cybr/conjur/conjurrc.go +++ b/pkg/cybr/conjur/conjurrc.go @@ -48,12 +48,14 @@ func getPem(url string) (string, error) { } defer conn.Close() - if len(conn.ConnectionState().PeerCertificates) != 2 { + if len(conn.ConnectionState().PeerCertificates) == 1 { return "", fmt.Errorf("Invalid conjur url '%s'. Make sure hostname and port are correct", url) } - pemCert := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: conn.ConnectionState().PeerCertificates[0].Raw})) - secondPemCert := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: conn.ConnectionState().PeerCertificates[1].Raw})) - pemCert = pemCert + secondPemCert + + pemCert := "" + for _, cert := range conn.ConnectionState().PeerCertificates { + pemCert += string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) + } return pemCert, err } @@ -69,7 +71,7 @@ func createConjurCert(certFileName string, url string) error { reader := bufio.NewReader(os.Stdin) fmt.Print(fmt.Sprintf("Replace certificate file '%s' [y]: ", certFileName)) text, _ := reader.ReadString('\n') - answer := strings.Replace(text, "\n", "", -1) + answer := strings.TrimSpace(text) // overwrite file if answer == "" || answer == "y" { err = ioutil.WriteFile(certFileName, []byte(pemCert), 0600) @@ -85,7 +87,7 @@ func createConjurRcFile(account string, url string, certFileName string, conjurr fmt.Print("Replace ~/.conjurrc file [y]: ") reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') - answer := strings.Replace(text, "\n", "", -1) + answer := strings.TrimSpace(text) // overwrite ~/.conjurrc file if answer == "" || answer == "y" { diff --git a/pkg/cybr/conjur/netrc.go b/pkg/cybr/conjur/netrc.go index 136fe6d..ed6c9f2 100644 --- a/pkg/cybr/conjur/netrc.go +++ b/pkg/cybr/conjur/netrc.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "strings" ) @@ -13,6 +14,11 @@ var netrcTemplate string = `machine {{ APPLIANCE_URL }}/authn password {{ PASSWORD }} ` +// GetNetRcPath returns path to the ~/.conjurrc file os-agnostic +func GetNetRcPath(homeDir string) string { + return filepath.FromSlash(fmt.Sprintf("%s/.netrc", homeDir)) +} + // CreateNetRc create a conjur netrc file func CreateNetRc(username string, password string) error { // creatr ~/.netrc pas @@ -21,19 +27,19 @@ func CreateNetRc(username string, password string) error { return err } - conjurrcFileName := fmt.Sprintf("%s/.conjurrc", homeDir) + conjurrcFileName := GetConjurRcPath(homeDir) url := GetURLFromConjurRc(conjurrcFileName) if url == "" { return fmt.Errorf("Failed to get appliance url from '%s'. Run 'cam init' to set this file", conjurrcFileName) } // create the ~/.netrc file - netrcFileName := fmt.Sprintf("%s/.netrc", homeDir) + netrcFileName := GetNetRcPath(homeDir) fmt.Print("Replace ~/.netrc file [y]: ") // prompt user reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') - answer := strings.Replace(text, "\n", "", -1) + answer := strings.TrimSpace(text) if answer == "" || answer == "y" { // create the ~/.netrc file netrcContent := strings.Replace(netrcTemplate, "{{ USERNAME }}", username, 1)