├── VERSION.md ├── doc ├── kion-cli-hero.png ├── kion-cli-usage.gif ├── config_sop.md ├── release_sop.md └── man1 │ └── kion.1 ├── lib ├── helper │ ├── helper.go │ ├── prompts.go │ ├── configuration.go │ ├── shell.go │ ├── output_test.go │ ├── output.go │ ├── wizards.go │ ├── helper_test.go │ ├── transform_test.go │ ├── transform.go │ └── browser.go ├── structs │ ├── structs.go │ ├── session-structs.go │ ├── helper-structs.go │ └── configuration-structs.go ├── defaults │ ├── defaults.go │ └── defaults.yml ├── kion │ ├── private.go │ ├── console.go │ ├── stak.go │ ├── project.go │ ├── account.go │ ├── auth.go │ ├── favorite.go │ ├── kion.go │ └── car.go ├── cache │ ├── helpers-cache.go │ ├── cache.go │ ├── password-cache.go │ ├── session-cache.go │ └── stak-cache.go └── commands │ ├── console.go │ ├── run.go │ ├── stak.go │ ├── favorite.go │ ├── authentication.go │ ├── utility.go │ └── commands.go ├── _kion ├── LICENSE ├── go.mod ├── go.sum ├── main.go └── CHANGELOG.md /VERSION.md: -------------------------------------------------------------------------------- 1 | v0.14.0 2 | -------------------------------------------------------------------------------- /doc/kion-cli-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kionsoftware/kion-cli/HEAD/doc/kion-cli-hero.png -------------------------------------------------------------------------------- /doc/kion-cli-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kionsoftware/kion-cli/HEAD/doc/kion-cli-usage.gif -------------------------------------------------------------------------------- /lib/helper/helper.go: -------------------------------------------------------------------------------- 1 | // Package helper provides utility functions for various common tasks. 2 | package helper 3 | -------------------------------------------------------------------------------- /lib/structs/structs.go: -------------------------------------------------------------------------------- 1 | // Package structs provides the data structures used across the application. 2 | package structs 3 | -------------------------------------------------------------------------------- /_kion: -------------------------------------------------------------------------------- 1 | #compdef kion 2 | 3 | _cli_zsh_autocomplete() { 4 | local -a opts 5 | local cur 6 | cur=${words[-1]} 7 | if [[ "$cur" == "-"* ]]; then 8 | opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") 9 | else 10 | opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") 11 | fi 12 | 13 | if [[ "${opts[1]}" != "" ]]; then 14 | _describe 'values' opts 15 | else 16 | _files 17 | fi 18 | } 19 | 20 | compdef _cli_zsh_autocomplete kion 21 | -------------------------------------------------------------------------------- /lib/defaults/defaults.go: -------------------------------------------------------------------------------- 1 | // Package defaults provides access to the default configuration file embedded 2 | // in the binary. This file is used to initialize the configuration file for 3 | // organizations that want to distribute a binary with custom defaults. 4 | package defaults 5 | 6 | import ( 7 | "embed" 8 | ) 9 | 10 | //go:embed defaults.yml 11 | var ConfigFS embed.FS 12 | 13 | // GetDefaultConfig returns the default configuration. 14 | func GetDefaultConfig() ([]byte, error) { 15 | return ConfigFS.ReadFile("defaults.yml") 16 | } 17 | -------------------------------------------------------------------------------- /lib/structs/session-structs.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // // 5 | // Structs // 6 | // // 7 | //////////////////////////////////////////////////////////////////////////////// 8 | 9 | // SessionInfo holds the information about the federated session. 10 | type SessionInfo struct { 11 | AccountName string 12 | AccountNumber string 13 | AccountTypeID uint 14 | AwsIamRoleName string 15 | Region string 16 | } 17 | -------------------------------------------------------------------------------- /lib/structs/helper-structs.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | // FavoritesComparison holds the results of comparing local favorites with API 4 | // favorites. It includes all favorites, exact matches, non-matches, conflicts, 5 | // and local-only favorites. It's returned by the CombineFavorites function. 6 | type FavoritesComparison struct { 7 | All []Favorite // Combined local + API, deduplicated and deconflicted 8 | ConflictsLocal []Favorite // Name conflicts (same name, different settings) 9 | ConflictsUpstream []Favorite // Name conflicts (same name, different settings) 10 | LocalOnly []Favorite // Local-only favorites (not matched in API) 11 | UnaliasedLocal []Favorite // Local favorites that update unnamed API favorites 12 | UnaliasedUpstream []Favorite // Local favorites that update unnamed API favorites 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nor Labs, Inc., DBA Kion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /doc/config_sop.md: -------------------------------------------------------------------------------- 1 | Config SOP 2 | ========== 3 | 4 | When adding new configuration options there are a few things to keep in mind in 5 | order to support several config features and established operational behavior. 6 | This SOP ensures the appropriate precedence is followed and users can 7 | intuitively use new options in ways they have established with existing ones. 8 | This SOP assumes you are adding an option to the configuration file that will 9 | also be a global flag / option. 10 | 11 | 1. Add the option to the configuration struct `lib/structs/configuraton-structs.go` 12 | 2. Add the option to the defaults override file `lib/defaults/defaults.yml` only if it is non-sensitive in nature 13 | 3. Set the option as a flag in `main.go`, this is what handles precedence as documented in the repo `README.md` file 14 | 4. If a non string var add a manual re-setting of it for when profiles are switched in `lib/commands/commands.go` 15 | 5. Test to ensure precedence is being followed as expected: 16 | 1. No `defaults.yml` entry, no config file 17 | 2. Then add an override in `defaults.yml` 18 | 3. Then add an override entry in your `~/.kion.yml` 19 | 4. Then add an override in the environment variable 20 | 5. Then add an override in the flag option 21 | -------------------------------------------------------------------------------- /lib/kion/private.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // ConsoleAccessCAR maps to the Kion API response for CAR data. 9 | type ConsoleAccessCAR struct { 10 | CARName string `json:"name"` 11 | CARID uint `json:"id"` 12 | CARRoleType string `json:"role_type"` 13 | Accounts []Account `json:"accounts"` 14 | ConsoleAccess bool `json:"console_access"` 15 | STAKAccess bool `json:"short_term_key_access"` 16 | LTAKAccess bool `json:"long_term_key_access"` 17 | AwsIamRoleName string `json:"aws_iam_role_name"` 18 | } 19 | 20 | // GetConsoleAccessCARS hits the private API endpoint to gather all cloud 21 | // access roles a user has access to. This method should only be used as a 22 | // fallback. 23 | func GetConsoleAccessCARS(host string, token string, projID uint) ([]ConsoleAccessCAR, error) { 24 | // build our query and get response 25 | url := fmt.Sprintf("%v/api/v1/project/%v/console-access", host, projID) 26 | query := map[string]string{} 27 | var data any 28 | resp, _, err := runQuery("GET", url, token, query, data) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // unmarshal response body 34 | var consoleAccessCars []ConsoleAccessCAR 35 | err = json.Unmarshal(resp.Data, &consoleAccessCars) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return consoleAccessCars, nil 41 | } 42 | -------------------------------------------------------------------------------- /doc/release_sop.md: -------------------------------------------------------------------------------- 1 | Release SOP 2 | =========== 3 | 4 | 1. Bump `VERSION.md` file according to [semver](https://semver.org/) practices 5 | 2. Create section in `CHANGELOG.md` that matches the new version 6 | - Write release paragraph at the top of the new release section in changelog 7 | - Ensure all non-backend, non-administrative PR's are accounted for in subsections (changed, added, etc) 8 | - Stub out new placeholder for next release at top of changelog 9 | 3. Verify all documentation is in place for the new features: 10 | - Repo `README.md` file 11 | - The `kion.1` and/or other man page(s) 12 | 3. Push and merge into main 13 | 4. Checkout main locally, pull from upstream 14 | 5. Tag with the release version and push 15 | ```bash 16 | git tag -a v0.5.0 -m "Release v0.5.0" 17 | git push origin v0.5.0 18 | ``` 19 | 6. Monitor release pipeline and ensure it passes, along with the brew repo pipeline 20 | 7. If there is an issue with a pipeline 21 | ```bash 22 | # pull tag, fix an issue, re-tag (tag v0.5.0 for example) 23 | 24 | git push origin :refs/tags/v0.5.0 # removes the tag upstream 25 | 26 | # do git changes and git push origin [branch] then pr back into master 27 | 28 | git tag -fa v0.5.0 -m "Release v0.5.0" # force a move / retag 29 | git push origin v0.5.0 30 | ``` 31 | 8. Validate the release 32 | - Homebrew repo shows new commit and the formula has been updated 33 | - Cli repo shows new release and release notes look correct 34 | - Upgrade locally via brew `brew update && brew upgrade kionsoftware/tap/kion-cli && kion --version` 35 | -------------------------------------------------------------------------------- /lib/helper/prompts.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "github.com/AlecAivazis/survey/v2" 4 | 5 | //////////////////////////////////////////////////////////////////////////////// 6 | // // 7 | // Prompts // 8 | // // 9 | //////////////////////////////////////////////////////////////////////////////// 10 | 11 | // surveyFormat sets survey icon and color configs. 12 | var surveyFormat = survey.WithIcons(func(icons *survey.IconSet) { 13 | icons.Question.Text = "" 14 | icons.Question.Format = "default+hb" 15 | }) 16 | 17 | // PromptSelect prompts the user to select from a slice of options. It requires 18 | // that the selection made be one of the options provided. 19 | func PromptSelect(message string, options []string) (string, error) { 20 | selection := "" 21 | prompt := &survey.Select{ 22 | Message: message, 23 | Options: options, 24 | } 25 | err := survey.AskOne(prompt, &selection, surveyFormat) 26 | return selection, err 27 | } 28 | 29 | // PromptInput prompts the user to provide dynamic input. 30 | func PromptInput(message string) (string, error) { 31 | var input string 32 | pi := &survey.Input{ 33 | Message: message, 34 | } 35 | err := survey.AskOne(pi, &input, surveyFormat, survey.WithValidator(survey.Required)) 36 | return input, err 37 | } 38 | 39 | // PromptPassword prompts the user to provide sensitive dynamic input. 40 | func PromptPassword(message string) (string, error) { 41 | var input string 42 | pi := &survey.Password{ 43 | Message: message, 44 | } 45 | err := survey.AskOne(pi, &input, surveyFormat, survey.WithValidator(survey.Required)) 46 | return input, err 47 | } 48 | -------------------------------------------------------------------------------- /lib/cache/helpers-cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/99designs/keyring" 7 | ) 8 | 9 | // flushCache clears the Kion CLI cache. 10 | func flushCache(k keyring.Keyring) error { 11 | // marshal an empty cache to json 12 | var cacheData CacheData 13 | data, err := json.Marshal(cacheData) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | // build the keyring item 19 | cacheName := "Kion-CLI Cache" 20 | cache := keyring.Item{ 21 | Key: cacheName, 22 | Data: data, 23 | Label: cacheName, 24 | Description: "Cache data for the Kion-CLI.", 25 | } 26 | 27 | // store the cache 28 | err = k.Set(cache) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | //////////////////////////////////////////////////////////////////////////////// 37 | // // 38 | // Real Cacher // 39 | // // 40 | //////////////////////////////////////////////////////////////////////////////// 41 | 42 | // FLushCache implements the FlushCache interface for RealCache. 43 | func (c *RealCache) FlushCache() error { 44 | return flushCache(c.keyring) 45 | } 46 | 47 | //////////////////////////////////////////////////////////////////////////////// 48 | // // 49 | // Null Cacher // 50 | // // 51 | //////////////////////////////////////////////////////////////////////////////// 52 | 53 | // FLushCache implements the FlushCache interface for NullCache. 54 | // This is just to satisfy the interface and should never be called. 55 | func (c *NullCache) FlushCache() error { 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /lib/kion/console.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | // // 10 | // Console // 11 | // // 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | // URLRequest maps to the required post body when interfacing with the Kion 15 | // API. 16 | type URLRequest struct { 17 | AccountID uint `json:"account_id"` 18 | AccountName string `json:"account_name"` 19 | AccountNumber string `json:"account_number"` 20 | AWSIAMRoleName string `json:"aws_iam_role_name"` 21 | AccountTypeID uint `json:"account_type_id"` 22 | RoleID uint `json:"role_id"` 23 | RoleType string `json:"role_type"` 24 | } 25 | 26 | // GetFederationURL queries the Kion API to generate a federation URL. 27 | func GetFederationURL(host string, token string, car CAR) (string, error) { 28 | // converting cloud access role type to role type 29 | var roleType string 30 | switch car.CloudAccessRoleType { 31 | case "inherited": 32 | roleType = "ou" 33 | case "local": 34 | roleType = "project" 35 | } 36 | 37 | // build our query and get response 38 | url := fmt.Sprintf("%v/api/v1/console-access", host) 39 | query := map[string]string{} 40 | data := URLRequest{ 41 | AccountID: car.AccountID, 42 | AccountName: car.AccountName, 43 | AccountNumber: car.AccountNumber, 44 | AWSIAMRoleName: car.AwsIamRoleName, 45 | AccountTypeID: car.AccountTypeID, 46 | RoleID: car.ID, 47 | RoleType: roleType, 48 | } 49 | resp, _, err := runQuery("POST", url, token, query, data) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | var fedurl string 55 | err = json.Unmarshal(resp.Data, &fedurl) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | return fedurl, nil 61 | } 62 | -------------------------------------------------------------------------------- /lib/helper/configuration.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kionsoftware/kion-cli/lib/defaults" 9 | "github.com/kionsoftware/kion-cli/lib/structs" 10 | 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | //////////////////////////////////////////////////////////////////////////////// 15 | // // 16 | // Configuration // 17 | // // 18 | //////////////////////////////////////////////////////////////////////////////// 19 | 20 | // LoadConfig reads in the embedded configuration file as well as the users 21 | // configuration yaml file located at `configFile`. Precedence is given to the 22 | // users configuration file, so any values set there will override the embedded 23 | // defaults. 24 | func LoadConfig(filename string, config *structs.Configuration) error { 25 | // read embedded defaults into the config 26 | defaultConfig, err := defaults.GetDefaultConfig() 27 | if err == nil { 28 | // only try to parse if we successfully got the embedded config 29 | if err := yaml.Unmarshal(defaultConfig, &config); err != nil { 30 | return fmt.Errorf("failed to parse embedded configuration: %w", err) 31 | } 32 | } 33 | 34 | // try to read users config file 35 | data, err := os.ReadFile(filename) 36 | if err != nil { 37 | // if file doesn't exist, just use embedded defaults 38 | if errors.Is(err, os.ErrNotExist) { 39 | return nil 40 | } 41 | return err 42 | } 43 | 44 | // parse external config and override defaults 45 | if err := yaml.Unmarshal(data, config); err != nil { 46 | return fmt.Errorf("failed to parse config file %s: %w", filename, err) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // SaveConfig saves the entirety of the current config to the users config file. 53 | func SaveConfig(filename string, config structs.Configuration) error { 54 | // marshal to yaml 55 | bytes, err := yaml.Marshal(config) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | // write it out 61 | return os.WriteFile(filename, bytes, 0644) 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kionsoftware/kion-cli 2 | 3 | go 1.24.4 4 | 5 | require ( 6 | github.com/99designs/keyring v1.2.2 7 | github.com/AlecAivazis/survey/v2 v2.3.6 8 | github.com/fatih/color v1.15.0 9 | github.com/hashicorp/go-version v1.6.0 10 | github.com/russellhaering/gosaml2 v0.9.1 11 | github.com/russellhaering/goxmldsig v1.4.0 12 | github.com/urfave/cli/v2 v2.25.1 13 | gopkg.in/yaml.v2 v2.4.0 14 | ) 15 | 16 | require ( 17 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect 18 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 19 | github.com/beevik/etree v1.1.0 // indirect 20 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 21 | github.com/charmbracelet/lipgloss v1.1.0 // indirect 22 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 23 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 24 | github.com/charmbracelet/x/term v0.2.1 // indirect 25 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 26 | github.com/danieljoos/wincred v1.1.2 // indirect 27 | github.com/dvsekhvalnov/jose2go v1.6.0 // indirect 28 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect 29 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect 30 | github.com/jonboulle/clockwork v0.3.0 // indirect 31 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 32 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 33 | github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect 34 | github.com/mattn/go-colorable v0.1.13 // indirect 35 | github.com/mattn/go-isatty v0.0.20 // indirect 36 | github.com/mattn/go-runewidth v0.0.16 // indirect 37 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 38 | github.com/mtibben/percent v0.2.1 // indirect 39 | github.com/muesli/termenv v0.16.0 // indirect 40 | github.com/rivo/uniseg v0.4.7 // indirect 41 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 42 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 43 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 44 | golang.org/x/sys v0.30.0 // indirect 45 | golang.org/x/term v0.7.0 // indirect 46 | golang.org/x/text v0.9.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /lib/defaults/defaults.yml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ## ## 3 | ## Attention!! ## 4 | ## ## 5 | ## vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ## 6 | ## ## 7 | ## DO NOT STORE SENSITIVE CONFIGURATIONS IN THIS FILE. All contents will be ## 8 | ## stored as plain text inside the compiled Kion CLI binary & be accessible ## 9 | ## to any user with read access. ## 10 | ## ## 11 | ## ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ## 12 | ## ## 13 | ## Values stored here are overrides to the Kion CLI defaults (the config ## 14 | ## structs null values). This is provided as a convenience to create custom ## 15 | ## builds of the CLI for distribution within companies. Precedence will be ## 16 | ## as follows: ## 17 | ## ## 18 | ## Flag > Environment Variables > Config File > Default Values ## 19 | ## `------------' ## 20 | ## | ## 21 | ## .-------------------------------------------' ## 22 | ## V ## 23 | ## Default Values (THIS DEFAULTS.YML > config struct null values) ## 24 | ## ## 25 | ################################################################################ 26 | 27 | kion: 28 | url: "" 29 | idms_id: "" 30 | saml_metadata_file: "" 31 | saml_sp_issuer: "" 32 | saml_print_url: false 33 | disable_cache: false 34 | debug_mode: false 35 | quiet_mode: false 36 | -------------------------------------------------------------------------------- /lib/cache/cache.go: -------------------------------------------------------------------------------- 1 | // Package cache provides caching functionality for kion-cli using a keyring. 2 | package cache 3 | 4 | import ( 5 | "github.com/99designs/keyring" 6 | "github.com/kionsoftware/kion-cli/lib/kion" 7 | ) 8 | 9 | // Cache is an interface for storing and receiving data. 10 | type Cache interface { 11 | SetStak(carName string, accNum string, accAlias string, value kion.STAK) error 12 | GetStak(carName string, accNum string, accAlias string) (kion.STAK, bool, error) 13 | SetSession(value kion.Session) error 14 | GetSession() (kion.Session, bool, error) 15 | SetPassword(host string, idmsID uint, un string, pw string) error 16 | GetPassword(host string, idmsID uint, un string) (string, bool, error) 17 | FlushCache() error 18 | } 19 | 20 | //////////////////////////////////////////////////////////////////////////////// 21 | // // 22 | // Real Cacher // 23 | // // 24 | //////////////////////////////////////////////////////////////////////////////// 25 | 26 | // RealCache is our cache object for passing the keychain to receiver methods. 27 | type RealCache struct { 28 | keyring keyring.Keyring 29 | } 30 | 31 | // CacheData is a nested structure for storing kion-cli data. 32 | type CacheData struct { 33 | STAK map[string]kion.STAK 34 | SESSION kion.Session 35 | PASSWORD map[string]string 36 | } 37 | 38 | // NewCache creates a new RealCache. 39 | func NewCache(keyring keyring.Keyring) *RealCache { 40 | return &RealCache{ 41 | keyring: keyring, 42 | } 43 | } 44 | 45 | //////////////////////////////////////////////////////////////////////////////// 46 | // // 47 | // Null Cacher // 48 | // // 49 | //////////////////////////////////////////////////////////////////////////////// 50 | 51 | // NullCache implements the Cache interface and does nothing. 52 | type NullCache struct { 53 | } 54 | 55 | // NewNullCache creates a new NullCache. 56 | func NewNullCache() *NullCache { 57 | return &NullCache{} 58 | } 59 | -------------------------------------------------------------------------------- /lib/kion/stak.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | //////////////////////////////////////////////////////////////////////////////// 10 | // // 11 | // Short Term Access Keys // 12 | // // 13 | //////////////////////////////////////////////////////////////////////////////// 14 | 15 | // STAK maps to the Kion API response for short term access keys. 16 | type STAK struct { 17 | AccessKey string `json:"access_key"` 18 | SecretAccessKey string `json:"secret_access_key"` 19 | SessionToken string `json:"session_token"` 20 | Duration int64 `json:"duration"` 21 | Expiration time.Time 22 | } 23 | 24 | // STAKRequest maps to the required post body when interfacing with the Kion 25 | // API. 26 | type STAKRequest struct { 27 | AccountNumber string `json:"account_number"` 28 | AccountAlias string `json:"account_alias"` 29 | CARName string `json:"cloud_access_role_name"` 30 | } 31 | 32 | // GetSTAK queries the Kion API to generate short term access keys. Must pass 33 | // either an account number or an account alias, one can be "". 34 | func GetSTAK(host string, token string, carName string, accNum string, accAlias string) (STAK, error) { 35 | // only account number or account alias should be provided, use the account 36 | // number by default 37 | if accNum != "" && accAlias != "" { 38 | accAlias = "" 39 | } 40 | 41 | // build our query and get response 42 | url := fmt.Sprintf("%v/api/v3/temporary-credentials/cloud-access-role", host) 43 | query := map[string]string{} 44 | data := STAKRequest{ 45 | AccountNumber: accNum, 46 | AccountAlias: accAlias, 47 | CARName: carName, 48 | } 49 | resp, _, err := runQuery("POST", url, token, query, data) 50 | if err != nil { 51 | return STAK{}, err 52 | } 53 | 54 | // unmarshal response body 55 | var stak STAK 56 | err = json.Unmarshal(resp.Data, &stak) 57 | if err != nil { 58 | return STAK{}, err 59 | } 60 | 61 | // set the expiration time, buffer by 30 seconds 62 | duration := stak.Duration 63 | if duration == 0 { 64 | duration = 900 65 | } 66 | stak.Expiration = time.Now().Add(time.Duration(duration-30) * time.Second) 67 | 68 | return stak, nil 69 | } 70 | -------------------------------------------------------------------------------- /lib/commands/console.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/kionsoftware/kion-cli/lib/helper" 8 | "github.com/kionsoftware/kion-cli/lib/kion" 9 | "github.com/kionsoftware/kion-cli/lib/structs" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // FedConsole opens the CSP console for the selected account and cloud access 14 | // role in the user's default browser. 15 | func (c *Cmd) FedConsole(cCtx *cli.Context) error { 16 | // handle auth 17 | err := c.setAuthToken(cCtx) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | // retrieve the account number, account alias, and CAR name from the context 23 | accNum := cCtx.String("account") 24 | accountAlias := cCtx.String("alias") 25 | carName := cCtx.String("car") 26 | 27 | var car kion.CAR 28 | if carName != "" && (accNum != "" || accountAlias != "") { 29 | // fetch the car directly using account number or alias and car name 30 | if accNum != "" { 31 | car, err = kion.GetCARByNameAndAccount(c.config.Kion.URL, c.config.Kion.APIKey, carName, accNum) 32 | if err != nil { 33 | return fmt.Errorf("failed to get CAR for account %s and CAR %s: %v", accNum, carName, err) 34 | } 35 | } else { 36 | car, err = kion.GetCARByNameAndAlias(c.config.Kion.URL, c.config.Kion.APIKey, carName, accountAlias) 37 | if err != nil { 38 | return fmt.Errorf("failed to get CAR for alias %s and CAR %s: %v", accountAlias, carName, err) 39 | } 40 | } 41 | } else { 42 | // walk user through the prompt workflow to select a car 43 | err = helper.CARSelector(cCtx, &car) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | // grab the csp federation url 50 | url, err := kion.GetFederationURL(c.config.Kion.URL, c.config.Kion.APIKey, car) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // grab the second argument, used as a redirect parameter 56 | redirect := getSecondArgument(cCtx) 57 | 58 | // print out how to store as a favorite 59 | if !c.config.Kion.QuietMode { 60 | if err := helper.PrintFavoriteConfig(os.Stdout, car, "", "web"); err != nil { 61 | return err 62 | } 63 | } 64 | 65 | session := structs.SessionInfo{ 66 | AccountName: car.AccountName, 67 | AccountNumber: car.AccountNumber, 68 | AccountTypeID: car.AccountTypeID, 69 | AwsIamRoleName: car.AwsIamRoleName, 70 | } 71 | return helper.OpenBrowserRedirect(url, session, c.config.Browser, redirect, "") 72 | } 73 | -------------------------------------------------------------------------------- /lib/kion/project.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | // // 10 | // Projects // 11 | // // 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | // Project maps to the Kion API response for projects. 15 | type Project struct { 16 | Archived bool `json:"archived"` 17 | AutoPay bool `json:"auto_pay"` 18 | DefaultAwsRegion string `json:"default_aws_region"` 19 | Description string `json:"description"` 20 | ID uint `json:"id"` 21 | Name string `json:"name"` 22 | OuID uint `json:"ou_id"` 23 | } 24 | 25 | // GetProject queries the Kion API for a list of all projects within the application. 26 | func GetProjects(host string, token string) ([]Project, error) { 27 | // build our query and get response 28 | url := host + "/api/v3/project" 29 | query := map[string]string{} 30 | var data any 31 | resp, _, err := runQuery("GET", url, token, query, data) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | // unmarshal response body 37 | var projects []Project 38 | err = json.Unmarshal(resp.Data, &projects) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return projects, nil 44 | } 45 | 46 | // GetProjectByID returns the project for a given project ID. Note that if a 47 | // user has car access only to a project this will return a 403. To accommodate 48 | // users with minimal permissions test response codes and fallback accordingly 49 | // or use GetProjects which will work but be more verbose. 50 | func GetProjectByID(host string, token string, id uint) (Project, error) { 51 | // build our query and get response 52 | url := fmt.Sprintf("%v/api/v3/project/%v", host, id) 53 | query := map[string]string{} 54 | var data any 55 | resp, _, err := runQuery("GET", url, token, query, data) 56 | if err != nil { 57 | return Project{}, err 58 | } 59 | 60 | // unmarshal response body 61 | var project Project 62 | err = json.Unmarshal(resp.Data, &project) 63 | if err != nil { 64 | return Project{}, err 65 | } 66 | 67 | return project, nil 68 | } 69 | -------------------------------------------------------------------------------- /lib/commands/run.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/kionsoftware/kion-cli/lib/helper" 8 | "github.com/kionsoftware/kion-cli/lib/kion" 9 | "github.com/kionsoftware/kion-cli/lib/structs" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | // RunCommand generates creds for an AWS account then executes the user 14 | // provided command with said credentials set. 15 | func (c *Cmd) RunCommand(cCtx *cli.Context) error { 16 | // set vars for easier access 17 | favName := cCtx.String("favorite") 18 | accNum := cCtx.String("account") 19 | accAlias := cCtx.String("alias") 20 | carName := cCtx.String("car") 21 | region := c.config.Kion.DefaultRegion 22 | 23 | // placeholder for our stak 24 | var stak kion.STAK 25 | 26 | // prefer favorites if specified, else use account/alias and car 27 | if favName != "" { 28 | // map our favorites for ease of use 29 | _, fMap := helper.MapFavs(c.config.Favorites) 30 | 31 | // if arg passed is a valid favorite use it else error out 32 | var fav string 33 | var err error 34 | if fMap[favName] != (structs.Favorite{}) { 35 | fav = favName 36 | } else { 37 | return errors.New("can't find favorite") 38 | } 39 | 40 | // grab our favorite 41 | favorite := fMap[fav] 42 | 43 | // check if we have a valid cached stak else grab a new one 44 | cachedSTAK, found, err := c.cache.GetStak(favorite.CAR, favorite.Account, "") 45 | if err != nil { 46 | return err 47 | } 48 | if found && cachedSTAK.Expiration.After(time.Now().Add(-5*time.Second)) { 49 | stak = cachedSTAK 50 | } else { 51 | stak, err = c.authStakCache(cCtx, favorite.CAR, favorite.Account, "") 52 | if err != nil { 53 | return err 54 | } 55 | } 56 | 57 | // take the region flag over the favorite region 58 | targetRegion := region 59 | if targetRegion == "" { 60 | targetRegion = favorite.Region 61 | } 62 | 63 | // run the command 64 | err = helper.RunCommand(stak, targetRegion, cCtx.Args().First(), cCtx.Args().Tail()...) 65 | if err != nil { 66 | return err 67 | } 68 | } else { 69 | // check if we have a valid cached stak else grab a new one 70 | cachedSTAK, found, err := c.cache.GetStak(carName, accNum, accAlias) 71 | if err != nil { 72 | return err 73 | } 74 | if found && cachedSTAK.Expiration.After(time.Now().Add(-5*time.Second)) { 75 | stak = cachedSTAK 76 | } else { 77 | stak, err = c.authStakCache(cCtx, carName, accNum, accAlias) 78 | if err != nil { 79 | return err 80 | } 81 | } 82 | 83 | err = helper.RunCommand(stak, region, cCtx.Args().First(), cCtx.Args().Tail()...) 84 | if err != nil { 85 | return err 86 | } 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /lib/kion/account.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | // // 10 | // Accounts // 11 | // // 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | // Account maps to the Kion API response for account data. 15 | type Account struct { 16 | Email string `json:"account_email"` 17 | Name string `json:"account_name"` 18 | Alias string `json:"account_alias"` 19 | Number string `json:"account_number"` 20 | TypeID uint `json:"account_type_id"` 21 | ID uint `json:"id"` 22 | IncludeLinkedAccountSpend bool `json:"include_linked_account_spend"` 23 | LinkedAccountNumber string `json:"linked_account_number"` 24 | LinkedRole string `json:"linked_role"` 25 | PayerID uint `json:"payer_id"` 26 | ProjectID uint `json:"project_id"` 27 | SkipAccessChecking bool `json:"skip_access_checking"` 28 | UseOrgAccountInfo bool `json:"use_org_account_info"` 29 | } 30 | 31 | // GetAccountsOnProject returns a list of Accounts associated with a given Kion 32 | // project. 33 | func GetAccountsOnProject(host string, token string, id uint) ([]Account, int, error) { 34 | // build our query and get response 35 | url := fmt.Sprintf("%v/api/v3/project/%v/accounts", host, id) 36 | query := map[string]string{} 37 | var data any 38 | resp, statusCode, err := runQuery("GET", url, token, query, data) 39 | if err != nil { 40 | return nil, statusCode, err 41 | } 42 | 43 | // unmarshal response body 44 | var accounts []Account 45 | jsonErr := json.Unmarshal(resp.Data, &accounts) 46 | if jsonErr != nil { 47 | return nil, 0, err 48 | } 49 | 50 | return accounts, resp.Status, nil 51 | } 52 | 53 | // GetAccount returns an account by the given account number. 54 | func GetAccount(host string, token string, accountNum string) (*Account, int, error) { 55 | // build our query and get response 56 | url := fmt.Sprintf("%v/api/v3/account/by-account-number/%v", host, accountNum) 57 | query := map[string]string{} 58 | var data any 59 | resp, statusCode, err := runQuery("GET", url, token, query, data) 60 | if err != nil { 61 | return nil, statusCode, err 62 | } 63 | 64 | // unmarshal response body 65 | var account Account 66 | jsonErr := json.Unmarshal(resp.Data, &account) 67 | if jsonErr != nil { 68 | return nil, 0, err 69 | } 70 | 71 | return &account, resp.Status, nil 72 | } 73 | -------------------------------------------------------------------------------- /lib/structs/configuration-structs.go: -------------------------------------------------------------------------------- 1 | package structs 2 | 3 | //////////////////////////////////////////////////////////////////////////////// 4 | // // 5 | // Structs // 6 | // // 7 | //////////////////////////////////////////////////////////////////////////////// 8 | 9 | // Configuration holds the CLI tool values needed to run. The struct maps to 10 | // the applications configured dotfile for persistence between sessions. 11 | type Configuration struct { 12 | Kion Kion `yaml:"kion,omitempty"` 13 | Favorites []Favorite `yaml:"favorites,omitempty"` 14 | Profiles map[string]Profile `yaml:"profiles,omitempty"` 15 | Browser Browser `yaml:"browser,omitempty"` 16 | } 17 | 18 | // Kion holds information about the instance of Kion with which the application 19 | // interfaces with as well as the credentials to do so. 20 | type Kion struct { 21 | URL string `yaml:"url,omitempty"` 22 | APIKey string `yaml:"api_key,omitempty"` 23 | Username string `yaml:"username,omitempty"` 24 | Password string `yaml:"password,omitempty"` 25 | IDMS string `yaml:"idms_id,omitempty"` 26 | SamlMetadataFile string `yaml:"saml_metadata_file,omitempty"` 27 | SamlIssuer string `yaml:"saml_sp_issuer,omitempty"` 28 | SamlPrintURL bool `yaml:"saml_print_url,omitempty"` 29 | DisableCache bool `yaml:"disable_cache,omitempty"` 30 | DefaultRegion string `yaml:"default_region,omitempty"` 31 | DebugMode bool `yaml:"debug_mode,omitempty"` 32 | QuietMode bool `yaml:"quiet_mode,omitempty"` 33 | } 34 | 35 | // Favorite holds information about user defined favorites used to quickly 36 | // access desired accounts. 37 | type Favorite struct { 38 | Name string `yaml:"name,omitempty" json:"alias_name"` 39 | Account string `yaml:"account,omitempty" json:"account_number"` 40 | CAR string `yaml:"cloud_access_role,omitempty" json:"cloud_access_role_name"` 41 | AccessType string `yaml:"access_type,omitempty" json:"access_type"` 42 | Region string `yaml:"region,omitempty" json:"account_region"` 43 | Service string `yaml:"service,omitempty"` 44 | FirefoxContainerName string `yaml:"firefox_container_name,omitempty"` 45 | CloudServiceProvider string `json:"cloud_service_provider"` 46 | DescriptiveName string 47 | Unaliased bool 48 | } 49 | 50 | // Profile holds an alternate configuration for Kion and Favorites. 51 | type Profile struct { 52 | Kion Kion `yaml:"kion,omitempty"` 53 | Favorites []Favorite `yaml:"favorites,omitempty"` 54 | } 55 | 56 | // Browser holds configurations for browser options. 57 | type Browser struct { 58 | FirefoxContainers bool `yaml:"firefox_containers,omitempty"` 59 | CustomBrowserPath string `yaml:"custom_browser_path,omitempty"` 60 | } 61 | -------------------------------------------------------------------------------- /lib/kion/auth.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | // // 10 | // Auth // 11 | // // 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | // Session maps to the session data returned by Kion after authentication. 15 | type Session struct { 16 | // ID int `json:"id"` 17 | IDMSID uint 18 | UserName string 19 | // UserID int `json:"user_id"` 20 | Access struct { 21 | Expiry string `json:"expiry"` 22 | Token string `json:"token"` 23 | // UserID int `json:"user_id"` 24 | } `json:"access"` 25 | Refresh struct { 26 | Expiry string `json:"expiry"` 27 | Token string `json:"token"` 28 | // UserID int `json:"user_id"` 29 | } `json:"refresh"` 30 | } 31 | 32 | // IDMS maps to the Kion API response for configured IDMSs. 33 | type IDMS struct { 34 | ID uint `json:"id"` 35 | IdmsTypeID uint `json:"idms_type_id"` 36 | Name string `json:"name"` 37 | } 38 | 39 | // AuthRequest maps to the required post body when interfacing with the Kion 40 | // API. 41 | type AuthRequest struct { 42 | IDMSID uint `json:"idms"` 43 | Username string `json:"username"` 44 | Password string `json:"password"` 45 | } 46 | 47 | // GetIDMSs queries the Kion API for all configured IDMS systems with which a 48 | // user can authenticate via username and password. 49 | func GetIDMSs(host string) ([]IDMS, error) { 50 | // build our query and get response 51 | url := fmt.Sprintf("%v/api/v2/idms", host) 52 | query := map[string]string{} 53 | var data any 54 | resp, _, err := runQuery("GET", url, "", query, data) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | // unmarshal response body 60 | var idmss []IDMS 61 | err = json.Unmarshal(resp.Data, &idmss) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | // only pass along idms's that can accept username and password via kion 67 | unpwIdmss := []IDMS{} 68 | for _, idms := range idmss { 69 | if idms.IdmsTypeID == 1 || idms.IdmsTypeID == 2 { 70 | unpwIdmss = append(unpwIdmss, idms) 71 | } 72 | } 73 | 74 | return unpwIdmss, nil 75 | } 76 | 77 | // Authenticate queries the Kion API to authenticate a user via username and 78 | // password. 79 | func Authenticate(host string, idmsID uint, un string, pw string) (Session, error) { 80 | // build our query and get response 81 | url := fmt.Sprintf("%v/api/v3/token", host) 82 | query := map[string]string{} 83 | data := AuthRequest{ 84 | IDMSID: idmsID, 85 | Username: un, 86 | Password: pw, 87 | } 88 | resp, _, err := runQuery("POST", url, "", query, data) 89 | if err != nil { 90 | return Session{}, err 91 | } 92 | 93 | // unmarshal response body 94 | var session Session 95 | err = json.Unmarshal(resp.Data, &session) 96 | if err != nil { 97 | return Session{}, err 98 | } 99 | 100 | return session, nil 101 | } 102 | -------------------------------------------------------------------------------- /lib/kion/favorite.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/kionsoftware/kion-cli/lib/structs" 8 | ) 9 | 10 | //////////////////////////////////////////////////////////////////////////////// 11 | // // 12 | // Favorites // 13 | // // 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | // FavoritesResponse maps to the Kion API response. 17 | type FavoritesResponse struct { 18 | Status int `json:"status"` 19 | Favorites []structs.Favorite `json:"data"` 20 | } 21 | 22 | // GetAPIFavorites returns a list of a user's Favorites associated with a given Kion from the API 23 | func GetAPIFavorites(host string, token string) ([]structs.Favorite, int, error) { 24 | 25 | url := fmt.Sprintf("%v/api/v3/user-cloud-access-role-alias", host) 26 | query := map[string]string{} 27 | var data any 28 | resp, statusCode, err := runQuery("GET", url, token, query, data) 29 | if err != nil { 30 | return nil, statusCode, err 31 | } 32 | 33 | // unmarshal response body 34 | var favorites []structs.Favorite 35 | jsonErr := json.Unmarshal(resp.Data, &favorites) 36 | if jsonErr != nil { 37 | return nil, 0, err 38 | } 39 | 40 | var apiFavorites []structs.Favorite 41 | for _, apiFav := range favorites { 42 | 43 | // normalize the access type to match what the CLI uses 44 | apiFav.AccessType = ConvertAccessType(apiFav.AccessType) 45 | 46 | // handle upstream favorites with no alias 47 | if apiFav.Name == "" { 48 | apiFav.Name = "[unaliased]" 49 | apiFav.Unaliased = true 50 | } 51 | apiFavorites = append(apiFavorites, apiFav) 52 | } 53 | 54 | return apiFavorites, resp.Status, nil 55 | } 56 | 57 | func CreateFavorite(host string, token string, favorite structs.Favorite) (structs.Favorite, int, error) { 58 | url := fmt.Sprintf("%v/api/v3/user-cloud-access-role-alias", host) 59 | query := map[string]string{} 60 | data := map[string]string{ 61 | "alias_name": favorite.Name, 62 | "account_number": favorite.Account, 63 | "cloud_access_role_name": favorite.CAR, 64 | "access_type": favorite.AccessType, 65 | } 66 | resp, statusCode, err := runQuery("POST", url, token, query, data) 67 | if err != nil { 68 | return structs.Favorite{}, statusCode, err 69 | } 70 | 71 | // unmarshal response body 72 | var createdFav structs.Favorite 73 | jsonErr := json.Unmarshal(resp.Data, &createdFav) 74 | if jsonErr != nil { 75 | return structs.Favorite{}, 0, jsonErr 76 | } 77 | 78 | // check if the response is successful 79 | if statusCode != 201 && statusCode != 200 { 80 | return structs.Favorite{}, statusCode, fmt.Errorf("failed to create favorite: %s", resp.Message) 81 | } 82 | 83 | return createdFav, statusCode, nil 84 | } 85 | 86 | func DeleteFavorite(host string, token string, favoriteName string) (int, error) { 87 | url := fmt.Sprintf("%v/api/v3/user-cloud-access-role-alias", host) 88 | query := map[string]string{} 89 | data := map[string]string{"alias_name": favoriteName} 90 | resp, statusCode, err := runQuery("DELETE", url, token, query, data) 91 | if err != nil { 92 | return statusCode, err 93 | } 94 | 95 | // check if the response is successful 96 | if statusCode != 200 { 97 | return statusCode, fmt.Errorf("failed to delete favorite with name %s: %s", favoriteName, resp.Message) 98 | } 99 | 100 | return statusCode, nil 101 | } 102 | -------------------------------------------------------------------------------- /lib/cache/password-cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/99designs/keyring" 8 | ) 9 | 10 | //////////////////////////////////////////////////////////////////////////////// 11 | // // 12 | // Real Cacher // 13 | // // 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | // SetPassword stores a Password in the cache (or removes it if the password is nil). 17 | func (c *RealCache) SetPassword(host string, idmsID uint, un string, pw string) error { 18 | // set the key based on what was passed 19 | key := fmt.Sprintf("%s-%d-%s", host, idmsID, un) 20 | 21 | // pull our cache 22 | cacheName := "Kion-CLI Cache" 23 | cache, err := c.keyring.Get(cacheName) 24 | if err != nil && err != keyring.ErrKeyNotFound { 25 | return err 26 | } 27 | 28 | // unmarshal the json data 29 | var cacheData CacheData 30 | if len(cache.Data) > 0 { 31 | err = json.Unmarshal(cache.Data, &cacheData) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | // initialize the map if it is still nil 38 | if cacheData.PASSWORD == nil { 39 | cacheData.PASSWORD = make(map[string]string) 40 | } 41 | 42 | if pw != "" { 43 | // create/update our entry 44 | cacheData.PASSWORD[key] = pw 45 | } else { 46 | // Delete the entry 47 | delete(cacheData.PASSWORD, key) 48 | } 49 | 50 | // marshal the stack cache to json 51 | data, err := json.Marshal(cacheData) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | // build the keyring item 57 | cache = keyring.Item{ 58 | Key: cacheName, 59 | Data: data, 60 | Label: cacheName, 61 | Description: "Cache data for the Kion-CLI.", 62 | } 63 | 64 | // store the cache 65 | err = c.keyring.Set(cache) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // GetPassword retrieves a password from the cache. 74 | func (c *RealCache) GetPassword(host string, idmsID uint, un string) (string, bool, error) { 75 | // set the key based on what was passed 76 | key := fmt.Sprintf("%s-%d-%s", host, idmsID, un) 77 | 78 | // pull our cache 79 | cache, err := c.keyring.Get("Kion-CLI Cache") 80 | if err != nil { 81 | if err == keyring.ErrKeyNotFound { 82 | return "", false, nil 83 | } 84 | return "", false, err 85 | } 86 | 87 | // unmarshal the json data 88 | var cacheData CacheData 89 | if len(cache.Data) > 0 { 90 | err = json.Unmarshal(cache.Data, &cacheData) 91 | if err != nil { 92 | return "", false, err 93 | } 94 | } 95 | 96 | // return the password if found 97 | password, found := cacheData.PASSWORD[key] 98 | if found { 99 | return password, true, nil 100 | } 101 | 102 | // return empty password if not found 103 | return "", false, nil 104 | } 105 | 106 | //////////////////////////////////////////////////////////////////////////////// 107 | // // 108 | // Null Cacher // 109 | // // 110 | //////////////////////////////////////////////////////////////////////////////// 111 | 112 | // SetPassword does nothing. 113 | func (c *NullCache) SetPassword(host string, idmsID uint, un string, pw string) error { 114 | return nil 115 | } 116 | 117 | // GetPassword returns an empty password, false, and a nil error. 118 | func (c *NullCache) GetPassword(host string, idmsID uint, un string) (string, bool, error) { 119 | return "", false, nil 120 | } 121 | -------------------------------------------------------------------------------- /lib/commands/stak.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "github.com/kionsoftware/kion-cli/lib/helper" 8 | "github.com/kionsoftware/kion-cli/lib/kion" 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | // GenStaks generates short term access keys by walking users through an 13 | // interactive prompt. Short term access keys are either printed to stdout or a 14 | // sub-shell is created with them set in the environment. 15 | func (c *Cmd) GenStaks(cCtx *cli.Context) error { 16 | // stub out placeholders 17 | var car kion.CAR 18 | var stak kion.STAK 19 | 20 | // set vars for easier access 21 | endpoint := c.config.Kion.URL 22 | carName := cCtx.String("car") 23 | accNum := cCtx.String("account") 24 | accAlias := cCtx.String("alias") 25 | region := c.config.Kion.DefaultRegion 26 | 27 | // get command used and set cache validity buffer 28 | action, buffer := getActionAndBuffer(cCtx) 29 | 30 | // if we have what we need go look stuff up without prompts do it 31 | if (accNum != "" || accAlias != "") && carName != "" { 32 | // determine if we have a valid cached entry 33 | cachedSTAK, found, err := c.cache.GetStak(carName, accNum, accAlias) 34 | if err != nil { 35 | return err 36 | } 37 | getCar := true 38 | if found && cachedSTAK.Expiration.After(time.Now().Add(-buffer*time.Second)) { 39 | // cached stak found and is still valid 40 | stak = cachedSTAK 41 | if action != "subshell" && action != "save" { 42 | // skip getting the car for everything but subshell and save 43 | getCar = false 44 | } 45 | } 46 | 47 | // grab the car if needed 48 | if getCar { 49 | // handle auth 50 | err := c.setAuthToken(cCtx) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if accNum != "" { 56 | car, err = kion.GetCARByNameAndAccount(endpoint, c.config.Kion.APIKey, carName, accNum) 57 | if err != nil { 58 | return err 59 | } 60 | } else { 61 | car, err = kion.GetCARByNameAndAlias(endpoint, c.config.Kion.APIKey, carName, accAlias) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | } 67 | } else { 68 | // handle auth 69 | err := c.setAuthToken(cCtx) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // run through the car selector to fill any gaps 75 | err = helper.CARSelector(cCtx, &car) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | // rebuild cache key and determine if we have a valid cached entry 81 | cachedSTAK, found, err := c.cache.GetStak(car.Name, car.AccountNumber, "") 82 | if err != nil { 83 | return err 84 | } 85 | if found && cachedSTAK.Expiration.After(time.Now().Add(-buffer*time.Second)) { 86 | // cached stak found and is still valid 87 | stak = cachedSTAK 88 | } 89 | } 90 | 91 | // grab a new stak if needed 92 | if stak == (kion.STAK{}) { 93 | var err error 94 | stak, err = c.authStakCache(cCtx, car.Name, car.AccountNumber, car.AccountAlias) 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | 100 | // run the action 101 | switch action { 102 | case "credential-process": 103 | // NOTE: do not use os.Stderr here else credentials can be written to logs 104 | return helper.PrintCredentialProcess(os.Stdout, stak) 105 | case "print": 106 | return helper.PrintSTAK(os.Stdout, stak, region) 107 | case "save": 108 | return helper.SaveAWSCreds(stak, car) 109 | case "subshell": 110 | if !c.config.Kion.QuietMode { 111 | if err := helper.PrintFavoriteConfig(os.Stdout, car, region, "cli"); err != nil { 112 | return err 113 | } 114 | } 115 | var displayAlais string 116 | if accAlias != "" { 117 | displayAlais = accAlias 118 | } else { 119 | displayAlais = car.AccountName 120 | } 121 | return helper.CreateSubShell(car.AccountNumber, displayAlais, car.Name, stak, region) 122 | default: 123 | return nil 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/cache/session-cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/99designs/keyring" 7 | "github.com/kionsoftware/kion-cli/lib/kion" 8 | ) 9 | 10 | // SetSession is a common func for all Cache implementations and stores a 11 | // Session in the cache. 12 | func setSession(k keyring.Keyring, session kion.Session) error { 13 | // pull our stak cache 14 | cacheName := "Kion-CLI Cache" 15 | cache, err := k.Get(cacheName) 16 | if err != nil && err != keyring.ErrKeyNotFound { 17 | return err 18 | } 19 | 20 | // unmarshal the json data 21 | var cacheData CacheData 22 | if len(cache.Data) > 0 { 23 | err = json.Unmarshal(cache.Data, &cacheData) 24 | if err != nil { 25 | return err 26 | } 27 | } 28 | 29 | // store the session 30 | cacheData.SESSION = session 31 | 32 | // marshal the stack cache to json 33 | data, err := json.Marshal(cacheData) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | // build the keyring item 39 | cache = keyring.Item{ 40 | Key: cacheName, 41 | Data: data, 42 | Label: cacheName, 43 | Description: "Cache data for the Kion-CLI.", 44 | } 45 | 46 | // store the cache 47 | err = k.Set(cache) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | 54 | } 55 | 56 | // GetSession is a common func for all Cache implementations and retrieves a 57 | // Session in the cache. 58 | func getSession(k keyring.Keyring) (kion.Session, bool, error) { 59 | // pull our stak cache 60 | cache, err := k.Get("Kion-CLI Cache") 61 | if err != nil { 62 | if err == keyring.ErrKeyNotFound { 63 | return kion.Session{}, false, nil 64 | } 65 | return kion.Session{}, false, err 66 | } 67 | 68 | // unmarshal the json data 69 | var cacheData CacheData 70 | if len(cache.Data) > 0 { 71 | err = json.Unmarshal(cache.Data, &cacheData) 72 | if err != nil { 73 | return kion.Session{}, false, err 74 | } 75 | } 76 | 77 | // return the stak if found 78 | session := cacheData.SESSION 79 | if session != (kion.Session{}) { 80 | return session, true, nil 81 | } 82 | 83 | // return empty stak if not found 84 | return kion.Session{}, false, nil 85 | } 86 | 87 | //////////////////////////////////////////////////////////////////////////////// 88 | // // 89 | // Real Cacher // 90 | // // 91 | //////////////////////////////////////////////////////////////////////////////// 92 | 93 | // SetSession implements the Cache interface for RealCache and wraps a common 94 | // function for storing session data. 95 | func (c *RealCache) SetSession(session kion.Session) error { 96 | return setSession(c.keyring, session) 97 | } 98 | 99 | // GetSession implements the Cache interface for RealCache and wraps a common 100 | // function for retrieving session data. 101 | func (c *RealCache) GetSession() (kion.Session, bool, error) { 102 | return getSession(c.keyring) 103 | } 104 | 105 | //////////////////////////////////////////////////////////////////////////////// 106 | // // 107 | // Null Cacher // 108 | // // 109 | //////////////////////////////////////////////////////////////////////////////// 110 | 111 | // SetSession implements the Cache interface for NullCache and does nothing. 112 | func (c *NullCache) SetSession(session kion.Session) error { 113 | return nil 114 | } 115 | 116 | // GetSession implements the Cache interface for NullCache and returns an empty session, false, and a nil error. 117 | func (c *NullCache) GetSession() (kion.Session, bool, error) { 118 | return kion.Session{}, false, nil 119 | } 120 | -------------------------------------------------------------------------------- /lib/cache/stak-cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/99designs/keyring" 10 | "github.com/kionsoftware/kion-cli/lib/kion" 11 | ) 12 | 13 | //////////////////////////////////////////////////////////////////////////////// 14 | // // 15 | // Real Cacher // 16 | // // 17 | //////////////////////////////////////////////////////////////////////////////// 18 | 19 | // SetStak stores a STAK in the cache. 20 | func (c *RealCache) SetStak(carName string, accNum string, accAlias string, value kion.STAK) error { 21 | // set the key based on what was passed 22 | var key string 23 | if accNum != "" { 24 | key = fmt.Sprintf("%s-%s", carName, accNum) 25 | } else { 26 | key = fmt.Sprintf("%s-%s", carName, strings.ToLower(accAlias)) 27 | } 28 | 29 | // pull our stak cache 30 | cacheName := "Kion-CLI Cache" 31 | cache, err := c.keyring.Get(cacheName) 32 | if err != nil && err != keyring.ErrKeyNotFound { 33 | return err 34 | } 35 | 36 | // unmarshal the json data 37 | var cacheData CacheData 38 | if len(cache.Data) > 0 { 39 | err = json.Unmarshal(cache.Data, &cacheData) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | // initialize the map if it is still nil 46 | if cacheData.STAK == nil { 47 | cacheData.STAK = make(map[string]kion.STAK) 48 | } 49 | 50 | // clean expired entries 51 | now := time.Now() 52 | for key, stak := range cacheData.STAK { 53 | if stak.Expiration.Before(now) { 54 | delete(cacheData.STAK, key) 55 | } 56 | } 57 | 58 | // create our entry 59 | cacheData.STAK[key] = value 60 | 61 | // marshal the stack cache to json 62 | data, err := json.Marshal(cacheData) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | // build the keyring item 68 | cache = keyring.Item{ 69 | Key: cacheName, 70 | Data: data, 71 | Label: cacheName, 72 | Description: "Cache data for the Kion-CLI.", 73 | } 74 | 75 | // store the cache 76 | err = c.keyring.Set(cache) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // GetStak retrieves a STAK from the cache. 85 | func (c *RealCache) GetStak(carName string, accNum string, accAlias string) (kion.STAK, bool, error) { 86 | // set the key based on what was passed 87 | var key string 88 | if accNum != "" { 89 | key = fmt.Sprintf("%s-%s", carName, accNum) 90 | } else { 91 | key = fmt.Sprintf("%s-%s", carName, strings.ToLower(accAlias)) 92 | } 93 | 94 | // pull our stak cache 95 | cache, err := c.keyring.Get("Kion-CLI Cache") 96 | if err != nil { 97 | if err == keyring.ErrKeyNotFound { 98 | return kion.STAK{}, false, nil 99 | } 100 | return kion.STAK{}, false, err 101 | } 102 | 103 | // unmarshal the json data 104 | var cacheData CacheData 105 | if len(cache.Data) > 0 { 106 | err = json.Unmarshal(cache.Data, &cacheData) 107 | if err != nil { 108 | return kion.STAK{}, false, err 109 | } 110 | } 111 | 112 | // return the stak if found 113 | stak, found := cacheData.STAK[key] 114 | if found { 115 | return stak, true, nil 116 | } 117 | 118 | // return empty stak if not found 119 | return kion.STAK{}, false, nil 120 | } 121 | 122 | //////////////////////////////////////////////////////////////////////////////// 123 | // // 124 | // Null Cacher // 125 | // // 126 | //////////////////////////////////////////////////////////////////////////////// 127 | 128 | // SetStak does nothing. 129 | func (c *NullCache) SetStak(carName string, accNum string, accAlias string, value kion.STAK) error { 130 | return nil 131 | } 132 | 133 | // GetStak returns an empty STAK, false, and a nil error. 134 | func (c *NullCache) GetStak(carName string, accNum string, accAlias string) (kion.STAK, bool, error) { 135 | return kion.STAK{}, false, nil 136 | } 137 | -------------------------------------------------------------------------------- /lib/kion/kion.go: -------------------------------------------------------------------------------- 1 | // Package kion provides functions to interact with the Kion API. 2 | package kion 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | type APIRespBody struct { 14 | Status int `json:"status"` 15 | Message string `json:"message"` 16 | Data json.RawMessage `json:"data"` 17 | } 18 | 19 | //////////////////////////////////////////////////////////////////////////////// 20 | // // 21 | // Helpers // 22 | // // 23 | //////////////////////////////////////////////////////////////////////////////// 24 | 25 | // runQuery performs queries against the Kion API. 26 | func runQuery(method string, url string, token string, query map[string]string, payload any) (APIRespBody, int, error) { 27 | // prepare our response struct 28 | apiResp := APIRespBody{} 29 | 30 | // prepare the request body 31 | reqBody, err := json.Marshal(payload) 32 | if err != nil { 33 | return apiResp, 0, err 34 | } 35 | 36 | // start our request 37 | req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) 38 | if err != nil { 39 | return apiResp, 0, err 40 | } 41 | 42 | // append on our parameters to the req.URL.String() 43 | q := req.URL.Query() 44 | for key, value := range query { 45 | q.Add(key, value) 46 | } 47 | req.URL.RawQuery = q.Encode() 48 | 49 | // add authorization header to the req 50 | if token != "" { 51 | req.Header.Add("Authorization", "Bearer "+token) 52 | } 53 | 54 | // identify the source of the request 55 | req.Header.Add("kion-source", "kion-cli") 56 | 57 | // send the request 58 | client := &http.Client{} 59 | resp, err := client.Do(req) 60 | if err != nil { 61 | return apiResp, 0, err 62 | } 63 | defer resp.Body.Close() 64 | 65 | // get the body of the response 66 | respBody, err := io.ReadAll(resp.Body) 67 | if err != nil { 68 | return apiResp, 0, err 69 | } 70 | 71 | err = json.Unmarshal(respBody, &apiResp) 72 | if err != nil { 73 | return apiResp, resp.StatusCode, err 74 | } 75 | 76 | // handle non 200's 77 | if resp.StatusCode != 200 && resp.StatusCode != 201 { 78 | return apiResp, resp.StatusCode, fmt.Errorf("[%v] %v", resp.StatusCode, apiResp.Message) 79 | } 80 | 81 | // return the response 82 | return apiResp, resp.StatusCode, nil 83 | } 84 | 85 | //////////////////////////////////////////////////////////////////////////////// 86 | // // 87 | // Kion Configurations // 88 | // // 89 | //////////////////////////////////////////////////////////////////////////////// 90 | 91 | // GetVersion returns the targeted Kion's version number. 92 | func GetVersion(host string) (string, error) { 93 | url := fmt.Sprintf("%v/api/version", host) 94 | query := map[string]string{} 95 | var data any 96 | resp, _, err := runQuery("GET", url, "", query, data) 97 | if err != nil { 98 | return "", err 99 | } 100 | 101 | // unmarshal response body 102 | var version string 103 | err = json.Unmarshal(resp.Data, &version) 104 | if err != nil { 105 | return "", err 106 | } 107 | 108 | // remove any dev suffixes 109 | version = strings.Split(version, "-")[0] 110 | 111 | return version, nil 112 | } 113 | 114 | // GetSessionDuration returns the AWS session duration configuration Kion uses 115 | // to generate session tokens. If 403 is received, we assume the shortest 116 | // setting of 15 minutes. 117 | func GetSessionDuration(host string, token string) (int, error) { 118 | url := fmt.Sprintf("%v/api/v3/app-config/aws-access", host) 119 | query := map[string]string{} 120 | var data any 121 | resp, status, err := runQuery("GET", url, token, query, data) 122 | if err != nil { 123 | if status == 403 { 124 | return 15, nil 125 | } else { 126 | return 0, err 127 | } 128 | } 129 | 130 | // unmarshal response body 131 | var response struct { 132 | Duration int `json:"aws_temporary_credentials_duration"` 133 | } 134 | err = json.Unmarshal(resp.Data, &response) 135 | if err != nil { 136 | return 0, err 137 | } 138 | 139 | return response.Duration, nil 140 | } 141 | 142 | // ConvertAccessType converts the access type string between what the API uses 143 | // and the CLI. It converts "console_access" to "web", and vice versa, and 144 | // "short_term_key_access" to "cli" and vice versa. If the access type does 145 | // not match any of these, it returns the original string. 146 | func ConvertAccessType(accessType string) string { 147 | switch accessType { 148 | case "console_access": 149 | return "web" 150 | case "short_term_key_access": 151 | return "cli" 152 | case "web": 153 | return "console_access" 154 | case "cli": 155 | return "short_term_key_access" 156 | default: 157 | return accessType 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/helper/shell.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/fatih/color" 13 | "github.com/kionsoftware/kion-cli/lib/kion" 14 | ) 15 | 16 | //////////////////////////////////////////////////////////////////////////////// 17 | // // 18 | // Shell // 19 | // // 20 | //////////////////////////////////////////////////////////////////////////////// 21 | 22 | // CreateSubShell creates a sub-shell containing set variables for AWS short 23 | // term access keys. It attempts to use the users configured shell and rc file 24 | // while overriding the prompt to indicate the authed AWS account. 25 | func CreateSubShell(accountNumber string, accountAlias string, carName string, stak kion.STAK, region string) error { 26 | // check if we know the account name 27 | var accountMeta string 28 | var accountMetaSentence string 29 | if accountAlias == "" { 30 | accountMeta = accountNumber 31 | accountMetaSentence = accountNumber 32 | } else { 33 | if runtime.GOOS == "windows" { 34 | accountMeta = fmt.Sprintf("%v^|%v", accountAlias, accountNumber) 35 | } else { 36 | accountMeta = fmt.Sprintf("%v|%v", accountAlias, accountNumber) 37 | } 38 | accountMetaSentence = fmt.Sprintf("%v (%v)", accountAlias, accountNumber) 39 | } 40 | 41 | // get users shell information 42 | usrShellPath := os.Getenv("SHELL") 43 | usrShellName := filepath.Base(usrShellPath) 44 | usrHistFile := os.Getenv("HISTFILE") 45 | 46 | // create command based on the users shell and set prompt 47 | var cmd string 48 | switch usrShellName { 49 | case "zsh": 50 | zdotdir, err := os.MkdirTemp("", "kionzrootdir") 51 | if err != nil { 52 | return err 53 | } 54 | defer os.RemoveAll(zdotdir) 55 | f, err := os.Create(zdotdir + "/.zshrc") 56 | if err != nil { 57 | return err 58 | } 59 | fmt.Fprintf(f, `HISTFILE=%s; source $HOME/.zshrc; autoload -U colors && colors; export PS1="%%F{green}[%v]%%b%%f $PS1"`, usrHistFile, accountMeta) 60 | err = f.Sync() 61 | if err != nil { 62 | return err 63 | } 64 | cmd = fmt.Sprintf(`ZDOTDIR=%v zsh`, zdotdir) 65 | case "bash": 66 | cmd = fmt.Sprintf(`bash --rcfile <(echo "source \"$HOME/.bashrc\"; export PS1='[%v] > '")`, accountMeta) 67 | default: 68 | cmd = fmt.Sprintf(`bash --rcfile <(echo "source \"$HOME/.bashrc\"; export PS1='[%v] > '")`, accountMeta) 69 | } 70 | 71 | // init shell 72 | var shell *exec.Cmd 73 | if runtime.GOOS == "windows" && usrShellName == "" { 74 | cmdPath := "C:\\Windows\\System32\\cmd.exe" 75 | shell = exec.Command(cmdPath, "/K", fmt.Sprintf(`PROMPT $E[32m[%s]$E[0m$G`, accountMeta)) 76 | } else { 77 | shell = exec.Command("bash", "-c", cmd) 78 | } 79 | 80 | // replicate current env vars and add stak 81 | shell.Env = os.Environ() 82 | shell.Env = append(shell.Env, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", stak.AccessKey)) 83 | shell.Env = append(shell.Env, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", stak.SecretAccessKey)) 84 | shell.Env = append(shell.Env, fmt.Sprintf("AWS_SESSION_TOKEN=%s", stak.SessionToken)) 85 | shell.Env = append(shell.Env, fmt.Sprintf("KION_ACCOUNT_NUM=%s", accountNumber)) 86 | shell.Env = append(shell.Env, fmt.Sprintf("KION_ACCOUNT_ALIAS=%s", accountAlias)) 87 | shell.Env = append(shell.Env, fmt.Sprintf("KION_CAR=%s", carName)) 88 | 89 | // set region if one was passed 90 | if region != "" { 91 | shell.Env = append(shell.Env, fmt.Sprintf("AWS_REGION=%s", region)) 92 | } 93 | 94 | // configure file handlers 95 | shell.Stdin = os.Stdin 96 | shell.Stdout = os.Stdout 97 | shell.Stderr = os.Stderr 98 | 99 | // run the shell 100 | color.Green("Starting session for %v", accountMetaSentence) 101 | err := shell.Run() 102 | color.Green("Shutting down session for %v", accountMetaSentence) 103 | 104 | return err 105 | } 106 | 107 | // RunCommand executes a one time command with AWS credentials set within the 108 | // environment. Command output is sent directly to stdout / stderr. 109 | func RunCommand(stak kion.STAK, region string, cmd string, args ...string) error { 110 | // stub out an empty command stack 111 | newCmd := make([]string, 0) 112 | 113 | // if we can't find a binary, assume it's a shell alias and prep a sub-shell call, otherwise use the binary path 114 | binary, err := exec.LookPath(cmd) 115 | if len(binary) < 1 || err != nil { 116 | sh := os.Getenv("SHELL") 117 | if strings.HasSuffix(sh, "/bash") || strings.HasSuffix(sh, "/fish") || strings.HasSuffix(sh, "/zsh") || strings.HasSuffix(sh, "/ksh") { 118 | newCmd = append(newCmd, sh, "-i", "-c", cmd) 119 | } 120 | } else { 121 | newCmd = append(newCmd, binary) 122 | } 123 | 124 | // replicate current env vars and add stak 125 | env := os.Environ() 126 | env = append(env, fmt.Sprintf("AWS_ACCESS_KEY_ID=%s", stak.AccessKey)) 127 | env = append(env, fmt.Sprintf("AWS_SECRET_ACCESS_KEY=%s", stak.SecretAccessKey)) 128 | env = append(env, fmt.Sprintf("AWS_SESSION_TOKEN=%s", stak.SessionToken)) 129 | 130 | // set region if one was passed 131 | if region != "" { 132 | env = append(env, fmt.Sprintf("AWS_REGION=%s", region)) 133 | } 134 | 135 | // moosh it all together 136 | newCmd = append(newCmd, args...) 137 | 138 | err = syscall.Exec(newCmd[0], newCmd[0:], env) 139 | return err 140 | } 141 | -------------------------------------------------------------------------------- /lib/commands/favorite.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sort" 7 | "time" 8 | 9 | "github.com/kionsoftware/kion-cli/lib/helper" 10 | "github.com/kionsoftware/kion-cli/lib/kion" 11 | "github.com/kionsoftware/kion-cli/lib/structs" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | func (c *Cmd) getFavorites(cCtx *cli.Context) ([]structs.Favorite, error) { 16 | // get the combined list of favorites from the CLI config and the Kion API (if compatible) 17 | useAPI := cCtx.App.Metadata["useFavoritesAPI"].(bool) 18 | var apiFavorites []structs.Favorite 19 | var combinedFavorites []structs.Favorite 20 | var err error 21 | if useAPI { 22 | // Authenticate before making API calls 23 | err = c.setAuthToken(cCtx) 24 | if err != nil { 25 | return apiFavorites, err 26 | } 27 | apiFavorites, _, err = kion.GetAPIFavorites(c.config.Kion.URL, c.config.Kion.APIKey) 28 | if err != nil { 29 | fmt.Printf("Error retrieving favorites from API: %v\n", err) 30 | return apiFavorites, err 31 | } 32 | } 33 | combinedFavorites, _, err = helper.CombineFavorites(c.config.Favorites, apiFavorites) 34 | if err != nil { 35 | fmt.Printf("Error combining favorites: %v\n", err) 36 | return combinedFavorites, err 37 | } 38 | return combinedFavorites, nil 39 | } 40 | 41 | // ListFavorites prints out the users stored favorites and favorites from the 42 | // Kion API. Extra information is provided if the verbose flag is set. 43 | func (c *Cmd) ListFavorites(cCtx *cli.Context) error { 44 | 45 | favorites, err := c.getFavorites(cCtx) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // sort favorites by name 51 | sort.Slice(favorites, func(i, j int) bool { 52 | return favorites[i].Name < favorites[j].Name 53 | }) 54 | 55 | // print it out 56 | if cCtx.Bool("verbose") { 57 | for _, f := range favorites { 58 | accessType := f.AccessType 59 | if accessType == "" { 60 | accessType = "cli (Default)" 61 | } 62 | region := f.Region 63 | if region == "" { 64 | region = "[unset]" 65 | } 66 | fmt.Printf(" %v:\n account number: %v\n cloud access role: %v\n access type: %v\n region: %v\n", f.Name, f.Account, f.CAR, accessType, region) 67 | } 68 | } else { 69 | for _, f := range favorites { 70 | fmt.Printf(" %v\n", f.DescriptiveName) 71 | } 72 | } 73 | 74 | return nil 75 | } 76 | 77 | // Favorites generates short term access keys or launches the web console 78 | // from stored favorites. If a favorite is found that matches the passed 79 | // argument it is used, otherwise the user is walked through a wizard to make a 80 | // selection. 81 | func (c *Cmd) Favorites(cCtx *cli.Context) error { 82 | 83 | favorites, err := c.getFavorites(cCtx) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | // run favorites through MapFavs 89 | fNames, fMap := helper.MapFavs(favorites) 90 | 91 | // if arg passed is a valid favorite use it else prompt 92 | var fav string 93 | if fMap[cCtx.Args().First()] != (structs.Favorite{}) { 94 | fav = cCtx.Args().First() 95 | } else { 96 | fav, err = helper.PromptSelect("Choose a Favorite:", fNames) 97 | if err != nil { 98 | return err 99 | } 100 | } 101 | 102 | // grab the favorite object 103 | favorite := fMap[fav] 104 | 105 | // override access type if explicitly set 106 | if cCtx.String("access-type") != "" { 107 | favorite.AccessType = cCtx.String("access-type") 108 | } 109 | 110 | // have --web flag take precedence over access-type 111 | if cCtx.Bool("web") { 112 | favorite.AccessType = "web" 113 | } 114 | 115 | // determine favorite action, default to cli unless explicitly set to web 116 | if favorite.AccessType == "web" { 117 | // handle auth 118 | err = c.setAuthToken(cCtx) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | // attempt to find an exact match then fallback to the first match 124 | car, err := kion.GetCARByNameAndAccount(c.config.Kion.URL, c.config.Kion.APIKey, favorite.CAR, favorite.Account) 125 | if err != nil { 126 | car, err = kion.GetCARByName(c.config.Kion.URL, c.config.Kion.APIKey, favorite.CAR) 127 | if err != nil { 128 | return err 129 | } 130 | car.AccountNumber = favorite.Account 131 | } 132 | 133 | url, err := kion.GetFederationURL(c.config.Kion.URL, c.config.Kion.APIKey, car) 134 | if err != nil { 135 | return err 136 | } 137 | fmt.Printf("Federating into %s (%s) via %s\n", favorite.Name, favorite.Account, car.AwsIamRoleName) 138 | session := structs.SessionInfo{ 139 | AccountName: favorite.Name, 140 | AccountNumber: car.AccountNumber, 141 | AccountTypeID: car.AccountTypeID, 142 | AwsIamRoleName: car.AwsIamRoleName, 143 | Region: favorite.Region, 144 | } 145 | return helper.OpenBrowserRedirect(url, session, c.config.Browser, favorite.Service, favorite.FirefoxContainerName) 146 | } else { 147 | // placeholder for our stak 148 | var stak kion.STAK 149 | 150 | // determine action and set required cache validity buffer 151 | action, buffer := getActionAndBuffer(cCtx) 152 | 153 | // check if we have a valid cached stak else grab a new one 154 | cachedSTAK, found, err := c.cache.GetStak(favorite.CAR, favorite.Account, "") 155 | if err != nil { 156 | return err 157 | } 158 | if found && cachedSTAK.Expiration.After(time.Now().Add(-buffer*time.Second)) { 159 | stak = cachedSTAK 160 | } else { 161 | stak, err = c.authStakCache(cCtx, favorite.CAR, favorite.Account, "") 162 | if err != nil { 163 | return err 164 | } 165 | } 166 | 167 | // credential process output, print, or create sub-shell 168 | switch action { 169 | case "credential-process": 170 | // NOTE: Do not use os.Stderr here else credentials can be written to logs 171 | return helper.PrintCredentialProcess(os.Stdout, stak) 172 | case "print": 173 | return helper.PrintSTAK(os.Stdout, stak, favorite.Region) 174 | case "subshell": 175 | return helper.CreateSubShell(favorite.Account, favorite.Name, favorite.CAR, stak, favorite.Region) 176 | default: 177 | return nil 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/helper/output_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/kionsoftware/kion-cli/lib/kion" 11 | ) 12 | 13 | func TestPrintSTAK(t *testing.T) { 14 | tests := []struct { 15 | description string 16 | stak kion.STAK 17 | region string 18 | want string 19 | }{ 20 | { 21 | "Empty", 22 | kion.STAK{}, 23 | "", 24 | "export AWS_ACCESS_KEY_ID=\nexport AWS_SECRET_ACCESS_KEY=\nexport AWS_SESSION_TOKEN=\n", 25 | }, 26 | // { 27 | // "Panic Condition", 28 | // kion.STAK{}, 29 | // "panic", 30 | // }, 31 | { 32 | "Partial STAK", 33 | kion.STAK{ 34 | AccessKey: "", 35 | SecretAccessKey: "aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf", 36 | SessionToken: "", 37 | }, 38 | "", 39 | "export AWS_ACCESS_KEY_ID=\nexport AWS_SECRET_ACCESS_KEY=aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf\nexport AWS_SESSION_TOKEN=\n", 40 | }, 41 | { 42 | "Full STAK", 43 | kion.STAK{ 44 | AccessKey: "ASIAABCDEFGHIJ1K23LM", 45 | SecretAccessKey: "aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf", 46 | SessionToken: "AbcDEFghIJKlMNoPQrStuVwXYZabcDEfGhI1JklmNoPQRStu2VWXYZaBcd34ef+GH+IJKLmNOPQRSTU5VwxyzABcdeFGHIj6KlMNoPQ7rSTUvW8X9yZAbCD0ef+gHIJkLMnoPqrstUVwxyzAb1CD2e34fgHiJKlMnOPqr56STuvwXyzABcdEfgh7IJK+8LM91No2pqrSTuvWxyz3ABCdEFGH4ijklMNOP5qrs6TUvWxyz789abcDefgH12iJKlM3no4pQRs+5t6UVw7/xy+ZaBcdE+FGhIj8kLmnOpqrstuvw9xyzab1cD/ef23GhIjkLMNoPQrstuv=", 47 | }, 48 | "", 49 | "export AWS_ACCESS_KEY_ID=ASIAABCDEFGHIJ1K23LM\nexport AWS_SECRET_ACCESS_KEY=aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf\nexport AWS_SESSION_TOKEN=AbcDEFghIJKlMNoPQrStuVwXYZabcDEfGhI1JklmNoPQRStu2VWXYZaBcd34ef+GH+IJKLmNOPQRSTU5VwxyzABcdeFGHIj6KlMNoPQ7rSTUvW8X9yZAbCD0ef+gHIJkLMnoPqrstUVwxyzAb1CD2e34fgHiJKlMnOPqr56STuvwXyzABcdEfgh7IJK+8LM91No2pqrSTuvWxyz3ABCdEFGH4ijklMNOP5qrs6TUvWxyz789abcDefgH12iJKlM3no4pQRs+5t6UVw7/xy+ZaBcdE+FGhIj8kLmnOpqrstuvw9xyzab1cD/ef23GhIjkLMNoPQrstuv=\n", 50 | }, 51 | { 52 | "With Region", 53 | kion.STAK{ 54 | AccessKey: "ASIAABCDEFGHIJ1K23LM", 55 | SecretAccessKey: "aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf", 56 | SessionToken: "AbcDEFghIJKlMNoPQrStuVwXYZabcDEfGhI1JklmNoPQRStu2VWXYZaBcd34ef+GH+IJKLmNOPQRSTU5VwxyzABcdeFGHIj6KlMNoPQ7rSTUvW8X9yZAbCD0ef+gHIJkLMnoPqrstUVwxyzAb1CD2e34fgHiJKlMnOPqr56STuvwXyzABcdEfgh7IJK+8LM91No2pqrSTuvWxyz3ABCdEFGH4ijklMNOP5qrs6TUvWxyz789abcDefgH12iJKlM3no4pQRs+5t6UVw7/xy+ZaBcdE+FGhIj8kLmnOpqrstuvw9xyzab1cD/ef23GhIjkLMNoPQrstuv=", 57 | }, 58 | "us-gov-west-1", 59 | "export AWS_REGION=us-gov-west-1\nexport AWS_ACCESS_KEY_ID=ASIAABCDEFGHIJ1K23LM\nexport AWS_SECRET_ACCESS_KEY=aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf\nexport AWS_SESSION_TOKEN=AbcDEFghIJKlMNoPQrStuVwXYZabcDEfGhI1JklmNoPQRStu2VWXYZaBcd34ef+GH+IJKLmNOPQRSTU5VwxyzABcdeFGHIj6KlMNoPQ7rSTUvW8X9yZAbCD0ef+gHIJkLMnoPqrstUVwxyzAb1CD2e34fgHiJKlMnOPqr56STuvwXyzABcdEfgh7IJK+8LM91No2pqrSTuvWxyz3ABCdEFGH4ijklMNOP5qrs6TUvWxyz789abcDefgH12iJKlM3no4pQRs+5t6UVw7/xy+ZaBcdE+FGhIj8kLmnOpqrstuvw9xyzab1cD/ef23GhIjkLMNoPQrstuv=\n", 60 | }, 61 | // TODO: add test that would print SETs for windows 62 | } 63 | 64 | for _, test := range tests { 65 | t.Run(test.description, func(t *testing.T) { 66 | // defer func to handle panic in test 67 | defer func() { 68 | if test.want == "panic" { 69 | if r := recover(); r == nil { 70 | t.Errorf("function should panic") 71 | } 72 | } 73 | }() 74 | 75 | var output bytes.Buffer 76 | err := PrintSTAK(&output, test.stak, test.region) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | if test.want != "panic" && test.want != output.String() { 81 | t.Errorf("\ngot:\n %v\nwanted:\n %v", output.String(), test.want) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestPrintCredentialProcess(t *testing.T) { 88 | tests := []struct { 89 | description string 90 | stak kion.STAK 91 | }{ 92 | { 93 | "Partial STAK", 94 | kion.STAK{ 95 | AccessKey: "", 96 | SecretAccessKey: "aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf", 97 | SessionToken: "", 98 | Duration: 43200, 99 | Expiration: time.Now().Add(43200 * time.Second), 100 | }, 101 | }, 102 | { 103 | "Full STAK", 104 | kion.STAK{ 105 | AccessKey: "ASIAABCDEFGHIJ1K23LM", 106 | SecretAccessKey: "aBCDeFg1hijkl2m3NOPqr4StUvWxY56z7abc8DEf", 107 | SessionToken: "AbcDEFghIJKlMNoPQrStuVwXYZabcDEfGhI1JklmNoPQRStu2VWXYZaBcd34ef+GH+IJKLmNOPQRSTU5VwxyzABcdeFGHIj6KlMNoPQ7rSTUvW8X9yZAbCD0ef+gHIJkLMnoPqrstUVwxyzAb1CD2e34fgHiJKlMnOPqr56STuvwXyzABcdEfgh7IJK+8LM91No2pqrSTuvWxyz3ABCdEFGH4ijklMNOP5qrs6TUvWxyz789abcDefgH12iJKlM3no4pQRs+5t6UVw7/xy+ZaBcdE+FGhIj8kLmnOpqrstuvw9xyzab1cD/ef23GhIjkLMNoPQrstuv=", 108 | Duration: 3600, 109 | Expiration: time.Now().Add(3600 * time.Second), 110 | }, 111 | }, 112 | // TODO: add a test that would cause the json marshaling to fail 113 | } 114 | 115 | for _, test := range tests { 116 | t.Run(test.description, func(t *testing.T) { 117 | var buf bytes.Buffer 118 | err := PrintCredentialProcess(&buf, test.stak) 119 | if err != nil { 120 | t.Fatalf("PrintCredentialProcess returned an error: %v", err) 121 | } 122 | 123 | // unmarshal the output into a map 124 | var output map[string]any 125 | err = json.Unmarshal(buf.Bytes(), &output) 126 | if err != nil { 127 | t.Fatalf("Failed to unmarshal the output: %v", err) 128 | } 129 | 130 | // parse the expiration field 131 | expiration, err := time.Parse(time.RFC3339, output["Expiration"].(string)) 132 | if err != nil { 133 | t.Fatalf("Failed to parse the Expiration field: %v", err) 134 | } 135 | 136 | // check if the expiration field is within a 1-second tolerance 137 | now := time.Now() 138 | duration := test.stak.Duration 139 | if duration == 0 { 140 | duration = 900 141 | } 142 | hardExpiration := now.Add(time.Duration(duration) * time.Second) 143 | if expiration.Before(hardExpiration.Add(-1*time.Second)) || expiration.After(hardExpiration.Add(1*time.Second)) { 144 | t.Fatalf("The 'Expiration' field is not within the expected range") 145 | } 146 | 147 | expected := fmt.Sprintf("{\n \"Version\": 1,\n \"AccessKeyId\": \"%v\",\n \"SecretAccessKey\": \"%v\",\n \"SessionToken\": \"%v\",\n \"Expiration\": \"%v\"\n}\n", test.stak.AccessKey, test.stak.SecretAccessKey, test.stak.SessionToken, output["Expiration"].(string)) 148 | 149 | if buf.String() != expected { 150 | t.Fatalf("Expected %s, but got %s", expected, buf.String()) 151 | } 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/helper/output.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/user" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | "time" 15 | 16 | "github.com/fatih/color" 17 | "github.com/kionsoftware/kion-cli/lib/kion" 18 | ) 19 | 20 | //////////////////////////////////////////////////////////////////////////////// 21 | // // 22 | // Output // 23 | // // 24 | //////////////////////////////////////////////////////////////////////////////// 25 | 26 | // PrintSTAK prints out the short term access keys for AWS auth. 27 | func PrintSTAK(w io.Writer, stak kion.STAK, region string) error { 28 | // handle windows vs linux for exports 29 | var export string 30 | if runtime.GOOS == "windows" { 31 | export = "SET" 32 | } else { 33 | export = "export" 34 | } 35 | 36 | // conditionally print region 37 | if region != "" { 38 | fmt.Fprintf(w, "%v AWS_REGION=%v\n", export, region) 39 | } 40 | 41 | // print the stak 42 | fmt.Fprintf(w, "%v AWS_ACCESS_KEY_ID=%v\nexport AWS_SECRET_ACCESS_KEY=%v\nexport AWS_SESSION_TOKEN=%v\n", export, stak.AccessKey, stak.SecretAccessKey, stak.SessionToken) 43 | 44 | return nil 45 | } 46 | 47 | // PrintFavoriteConfig prints out how to save the current selection as a 48 | // favorite within the users configuration file. 49 | func PrintFavoriteConfig(w io.Writer, car kion.CAR, region string, access_type string) error { 50 | color.New(color.FgBlue).Fprintf(w, "\nTo save your selection as a favorite add the following to\nyour configuration file under the 'favorites:' section:\n") 51 | fmt.Fprintf(w, " - name: ") 52 | color.New(color.FgGreen).Fprintf(w, "[your favorite alias]\n") 53 | fmt.Fprintf(w, " account: %v\n cloud_access_role: %v\n", car.AccountNumber, car.Name) 54 | if region != "" { 55 | fmt.Fprintf(w, " region: %v\n", region) 56 | } 57 | fmt.Fprintf(w, " access_type: %v\n\n", access_type) 58 | 59 | return nil 60 | } 61 | 62 | // PrintCredentialProcess prints out the short term access keys for use with 63 | // AWS profiles as a credential process subsystem. 64 | func PrintCredentialProcess(w io.Writer, stak kion.STAK) error { 65 | // create the credentials struct 66 | credentials := struct { 67 | Version int 68 | AccessKeyId string 69 | SecretAccessKey string 70 | SessionToken string 71 | Expiration string 72 | }{ 73 | 1, 74 | stak.AccessKey, 75 | stak.SecretAccessKey, 76 | stak.SessionToken, 77 | stak.Expiration.Format(time.RFC3339), 78 | } 79 | 80 | // marshal the credentials to json 81 | jsonData, err := json.MarshalIndent(credentials, "", " ") 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // print the json data 87 | fmt.Fprintln(w, string(jsonData)) 88 | 89 | return nil 90 | } 91 | 92 | // SaveAWSCreds saves the short term access keys for AWS auth to the users AWS 93 | // configuration file. 94 | func SaveAWSCreds(stak kion.STAK, car kion.CAR) error { 95 | // get the current user home directory. 96 | user, err := user.Current() 97 | if err != nil { 98 | return err 99 | } 100 | 101 | // derive aws creds paths 102 | awsCredsDir := filepath.Join(user.HomeDir, ".aws") 103 | awsCredsFile := filepath.Join(user.HomeDir, ".aws/credentials") 104 | 105 | // if the folder or file does not exist, create them 106 | if _, err := os.Stat(awsCredsDir); os.IsNotExist(err) { 107 | // create directory 108 | errDir := os.MkdirAll(awsCredsDir, 0755) 109 | if errDir != nil { 110 | return err 111 | } 112 | } 113 | if _, err := os.Stat(awsCredsFile); os.IsNotExist(err) { 114 | err = os.WriteFile(awsCredsFile, []byte(""), 0600) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | 120 | // read in the creds file 121 | contents, err := os.ReadFile(awsCredsFile) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | // determine if the profile already exists 127 | profileName := fmt.Sprintf("[%v_%v]", car.AccountNumber, car.AwsIamRoleName) 128 | index := strings.Index(string(contents), profileName) 129 | 130 | // append the profile if it does not exist, else update it 131 | if index == -1 { 132 | f, err := os.OpenFile(awsCredsFile, os.O_APPEND|os.O_WRONLY, 0644) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | linebreak := "\n" 138 | if runtime.GOOS == "windows" { 139 | linebreak = "\r\n" 140 | } 141 | 142 | text := "" 143 | text += fmt.Sprintf(linebreak+"[%v_%v]"+linebreak, car.AccountNumber, car.AwsIamRoleName) 144 | text += fmt.Sprintf("aws_access_key_id=%v"+linebreak, stak.AccessKey) 145 | text += fmt.Sprintf("aws_secret_access_key=%v"+linebreak, stak.SecretAccessKey) 146 | text += fmt.Sprintf("aws_session_token=%v"+linebreak, stak.SessionToken) 147 | 148 | _, err = f.WriteString(text) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | err = f.Close() 154 | if err != nil { 155 | return err 156 | } 157 | } else { 158 | f, err := os.Open(awsCredsFile) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | started := false 164 | 165 | buf := "" 166 | 167 | scanner := bufio.NewScanner(f) 168 | for scanner.Scan() { 169 | t := scanner.Text() 170 | if strings.Contains(t, profileName) { 171 | started = true 172 | buf += fmt.Sprintln(t) 173 | continue 174 | } 175 | 176 | if started { 177 | if !strings.Contains(t, "=") { 178 | started = false 179 | buf += fmt.Sprintln(t) 180 | continue 181 | } 182 | 183 | switch true { 184 | case strings.Contains(t, "aws_access_key_id"): 185 | buf += fmt.Sprintln("aws_access_key_id=" + stak.AccessKey) 186 | case strings.Contains(t, "aws_secret_access_key"): 187 | buf += fmt.Sprintln("aws_secret_access_key=" + stak.SecretAccessKey) 188 | case strings.Contains(t, "aws_session_token"): 189 | buf += fmt.Sprintln("aws_session_token=" + stak.SessionToken) 190 | default: 191 | return errors.New("there is a problem with the aws credentials file") 192 | } 193 | continue 194 | } 195 | buf += fmt.Sprintln(t) 196 | } 197 | 198 | if err := scanner.Err(); err != nil { 199 | return err 200 | } 201 | 202 | err = os.WriteFile(awsCredsFile, []byte(buf), 0600) 203 | if err != nil { 204 | return err 205 | } 206 | } 207 | 208 | fmt.Println("Credentials updated in the file:", awsCredsFile) 209 | fmt.Printf("You can reference this profile using this flag: --profile %v_%v\n", car.AccountNumber, car.AwsIamRoleName) 210 | fmt.Printf("Example command: aws s3 ls --profile %v_%v\n", car.AccountNumber, car.AwsIamRoleName) 211 | 212 | return nil 213 | } 214 | -------------------------------------------------------------------------------- /lib/kion/car.go: -------------------------------------------------------------------------------- 1 | package kion 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | //////////////////////////////////////////////////////////////////////////////// 11 | // // 12 | // Cloud Access Roles // 13 | // // 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | // CAR maps to the Kion API response for cloud access roles. 17 | type CAR struct { 18 | AccountID uint `json:"account_id"` 19 | AccountNumber string `json:"account_number"` 20 | AccountType string `json:"account_type"` 21 | AccountTypeID uint `json:"account_type_id"` 22 | AccountName string `json:"account_name"` 23 | AccountAlias string `json:"account_alias"` 24 | ApplyToAllAccounts bool `json:"apply_to_all_accounts"` 25 | AwsIamPath string `json:"aws_iam_path"` 26 | AwsIamRoleName string `json:"aws_iam_role_name"` 27 | CloudAccessRoleType string `json:"cloud_access_role_type"` 28 | CreatedAt struct { 29 | Time time.Time `json:"Time"` 30 | Valid bool `json:"Valid"` 31 | } `json:"created_at"` 32 | DeletedAt struct { 33 | Time time.Time `json:"Time"` 34 | Valid bool `json:"Valid"` 35 | } `json:"deleted_at"` 36 | FutureAccounts bool `json:"future_accounts"` 37 | ID uint `json:"id"` 38 | LongTermAccessKeys bool `json:"long_term_access_keys"` 39 | Name string `json:"name"` 40 | ProjectID uint `json:"project_id"` 41 | ShortTermAccessKeys bool `json:"short_term_access_keys"` 42 | UpdatedAt struct { 43 | Time time.Time `json:"Time"` 44 | Valid bool `json:"Valid"` 45 | } `json:"updated_at"` 46 | WebAccess bool `json:"web_access"` 47 | } 48 | 49 | // GetCARS queries the Kion API for all cloud access roles to which the 50 | // authenticated user has access. Deleted CARs will be excluded. 51 | func GetCARS(host string, token string, alias string) ([]CAR, error) { 52 | // build our query and get response 53 | url := fmt.Sprintf("%v/api/v3/me/cloud-access-role", host) 54 | query := map[string]string{ 55 | "account_alias": alias, 56 | } 57 | var data any 58 | resp, _, err := runQuery("GET", url, token, query, data) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // unmarshal response body 64 | var allcars []CAR 65 | err = json.Unmarshal(resp.Data, &allcars) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | var cars []CAR 71 | for _, car := range allcars { 72 | if car.DeletedAt.Time.IsZero() { 73 | cars = append(cars, car) 74 | } 75 | } 76 | 77 | return cars, nil 78 | } 79 | 80 | // GetCARSOnProject returns all cloud access roles that match a given project and account. 81 | func GetCARSOnProject(host string, token string, projID uint, accID uint) ([]CAR, error) { 82 | allCars, err := GetCARS(host, token, "") 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | // reduce to cars that match project and account 88 | var cars []CAR 89 | for _, car := range allCars { 90 | if car.ProjectID == projID && car.AccountID == accID { 91 | cars = append(cars, car) 92 | } 93 | } 94 | 95 | return cars, nil 96 | } 97 | 98 | // GetCARSOnAccount returns all cloud access roles that match a given account. 99 | func GetCARSOnAccount(host string, token string, accID uint) ([]CAR, error) { 100 | allCars, err := GetCARS(host, token, "") 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | // reduce to cars that match project and account 106 | var cars []CAR 107 | for _, car := range allCars { 108 | if car.AccountID == accID { 109 | cars = append(cars, car) 110 | } 111 | } 112 | 113 | return cars, nil 114 | } 115 | 116 | // GetCARByName returns a car that matches a given name. IMPORTANT: please use 117 | // GetCARByNameAndAccount instead where possible as there are no constraints 118 | // against CARs with duplicate names, this function is kept as a convenience 119 | // and workaround for users on older version of Kion that have limited 120 | // permissions. 121 | func GetCARByName(host string, token string, carName string) (CAR, error) { 122 | allCars, err := GetCARS(host, token, "") 123 | if err != nil { 124 | return CAR{}, err 125 | } 126 | 127 | // find our match 128 | // TODO: build and return a slice of matching CARs, then on all references 129 | // should be updated to handle the slice and prompt users for selection or 130 | // test all for success silently 131 | for _, car := range allCars { 132 | if car.Name == carName { 133 | return car, nil 134 | } 135 | } 136 | 137 | return CAR{}, fmt.Errorf("unable to find car %v", carName) 138 | } 139 | 140 | // GetCARByNameAndAccount returns a car that matches by name and account number. 141 | func GetCARByNameAndAccount(host string, token string, carName string, accountNumber string) (CAR, error) { 142 | allCars, err := GetCARS(host, token, "") 143 | if err != nil { 144 | return CAR{}, err 145 | } 146 | 147 | // find our match 148 | for _, car := range allCars { 149 | if car.Name == carName && car.AccountNumber == accountNumber { 150 | return car, nil 151 | } 152 | } 153 | 154 | return CAR{}, fmt.Errorf("unable to find car %v with account number %v", carName, accountNumber) 155 | } 156 | 157 | // GetCARByNameAndAlias returns a car that matches by name and account alias. 158 | func GetCARByNameAndAlias(host string, token string, carName string, accountAlias string) (CAR, error) { 159 | allCars, err := GetCARS(host, token, accountAlias) 160 | if err != nil { 161 | return CAR{}, err 162 | } 163 | 164 | // find our match 165 | for _, car := range allCars { 166 | if car.Name == carName && strings.EqualFold(car.AccountAlias, accountAlias) { 167 | return car, nil 168 | } 169 | } 170 | 171 | return CAR{}, fmt.Errorf("unable to find car %v with account alias %v", carName, accountAlias) 172 | } 173 | 174 | // GetAllCARsByName returns a slice of cars that matches a given name. 175 | func GetAllCARsByName(host string, token string, carName string) ([]CAR, error) { 176 | allCars, err := GetCARS(host, token, "") 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | // find our matches 182 | var cars []CAR 183 | for _, car := range allCars { 184 | if car.Name == carName { 185 | account, _, err := GetAccount(host, token, car.AccountNumber) 186 | if err != nil { 187 | // TODO: this may not be what we want to do here, kept as info level log 188 | // fmt.Println(" unable to lookup an associated account:", car.AccountNumber) 189 | continue 190 | } 191 | car.AccountName = account.Name 192 | car.AccountTypeID = account.TypeID 193 | cars = append(cars, car) 194 | } 195 | } 196 | 197 | // return our slice of cars 198 | if len(cars) > 0 { 199 | return cars, nil 200 | } 201 | 202 | return nil, fmt.Errorf("unable to find car %v", carName) 203 | } 204 | -------------------------------------------------------------------------------- /lib/helper/wizards.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kionsoftware/kion-cli/lib/kion" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | //////////////////////////////////////////////////////////////////////////////// 11 | // // 12 | // Wizards // 13 | // // 14 | //////////////////////////////////////////////////////////////////////////////// 15 | 16 | // CARSelector is a wizard that walks a user through the selection of a 17 | // Project, then associated Accounts, then available Cloud Access Roles, to set 18 | // the user selected Cloud Access Role. Optional account number and or car name 19 | // can be passed via an existing car struct, the flow will dynamically ask what 20 | // is needed to be able to find the full car. 21 | func CARSelector(cCtx *cli.Context, car *kion.CAR) error { 22 | // get list of projects, then build list of names and lookup map 23 | projects, err := kion.GetProjects(cCtx.String("endpoint"), cCtx.String("token")) 24 | if err != nil { 25 | return err 26 | } 27 | pNames, pMap := MapProjects(projects) 28 | if len(pNames) == 0 { 29 | return fmt.Errorf("no projects found") 30 | } 31 | 32 | // prompt user to select a project 33 | project, err := PromptSelect("Choose a project:", pNames) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if cCtx.App.Metadata["useUpdatedCloudAccessRoleAPI"] == true { 39 | // TODO: consolidate on this logic when support for 3.9 drops, that will 40 | // give us one full support line of buffer 41 | 42 | // get all cars for authed user, works with min permission set 43 | cars, err := kion.GetCARS(cCtx.String("endpoint"), cCtx.String("token"), "") 44 | if err != nil { 45 | return err 46 | } 47 | aNames, aMap := MapAccountsFromCARS(cars, pMap[project].ID) 48 | if len(aNames) == 0 { 49 | return fmt.Errorf("no accounts found") 50 | } 51 | 52 | // prompt user to select an account 53 | account, err := PromptSelect("Choose an Account:", aNames) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // narrow it down to just cars associated with the account 59 | var carsFiltered []kion.CAR 60 | for _, carObj := range cars { 61 | if carObj.AccountNumber == aMap[account] { 62 | carsFiltered = append(carsFiltered, carObj) 63 | } 64 | } 65 | cNames, cMap := MapCAR(carsFiltered) 66 | if len(cNames) == 0 { 67 | return fmt.Errorf("you have no cloud access roles assigned") 68 | } 69 | 70 | // prompt user to select a car 71 | carname, err := PromptSelect("Choose a Cloud Access Role:", cNames) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // inject the metadata into the car 77 | car.Name = cMap[carname].Name 78 | car.AccountName = cMap[carname].AccountName 79 | car.AccountNumber = aMap[account] 80 | car.AccountTypeID = cMap[carname].AccountTypeID 81 | car.AccountID = cMap[carname].AccountID 82 | car.AwsIamRoleName = cMap[carname].AwsIamRoleName 83 | car.ID = cMap[carname].ID 84 | car.CloudAccessRoleType = cMap[carname].CloudAccessRoleType 85 | 86 | // return nil 87 | return nil 88 | } else { 89 | // get list of accounts on project, then build a list of names and lookup map 90 | accounts, statusCode, err := kion.GetAccountsOnProject(cCtx.String("endpoint"), cCtx.String("token"), pMap[project].ID) 91 | if err != nil { 92 | if statusCode == 403 { 93 | // if we're getting a 403 work around permissions bug by temp using private api 94 | return carSelectorPrivateAPI(cCtx, pMap, project, car) 95 | } else { 96 | return err 97 | } 98 | } 99 | aNames, aMap := MapAccounts(accounts) 100 | if len(aNames) == 0 { 101 | return fmt.Errorf("no accounts found") 102 | } 103 | 104 | // prompt user to select an account 105 | account, err := PromptSelect("Choose an Account:", aNames) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | // get a list of cloud access roles, then build a list of names and lookup map 111 | cars, err := kion.GetCARSOnProject(cCtx.String("endpoint"), cCtx.String("token"), pMap[project].ID, aMap[account].ID) 112 | if err != nil { 113 | return err 114 | } 115 | cNames, cMap := MapCAR(cars) 116 | if len(cNames) == 0 { 117 | return fmt.Errorf("no cloud access roles found") 118 | } 119 | 120 | // prompt user to select a car 121 | carname, err := PromptSelect("Choose a Cloud Access Role:", cNames) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | // inject the metadata into the car 127 | car.Name = cMap[carname].Name 128 | car.AccountName = cMap[carname].AccountName 129 | car.AccountNumber = aMap[account].Number 130 | car.AccountTypeID = aMap[account].TypeID 131 | car.AccountID = aMap[account].ID 132 | car.AwsIamRoleName = cMap[carname].AwsIamRoleName 133 | car.ID = cMap[carname].ID 134 | car.CloudAccessRoleType = cMap[carname].CloudAccessRoleType 135 | 136 | // return nil 137 | return nil 138 | } 139 | } 140 | 141 | // carSelectorPrivateAPI is a temp shim workaround to address a public API 142 | // permissions issue. CARSelector should be called directly which will the 143 | // forward to this function if needed. 144 | func carSelectorPrivateAPI(cCtx *cli.Context, pMap map[string]kion.Project, project string, car *kion.CAR) error { 145 | // hit private api endpoint to gather all users cars and their associated accounts 146 | caCARs, err := kion.GetConsoleAccessCARS(cCtx.String("endpoint"), cCtx.String("token"), pMap[project].ID) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | // build a consolidated list of accounts from all available CARS and slice of cars per account 152 | var accounts []kion.Account 153 | cMap := make(map[string]kion.ConsoleAccessCAR) 154 | aToCMap := make(map[string][]string) 155 | for _, car := range caCARs { 156 | cname := fmt.Sprintf("%v (%v)", car.CARName, car.CARID) 157 | cMap[cname] = car 158 | for _, account := range car.Accounts { 159 | name := fmt.Sprintf("%v (%v)", account.Name, account.Number) 160 | aToCMap[name] = append(aToCMap[account.Name], cname) 161 | found := false 162 | for _, a := range accounts { 163 | if a.ID == account.ID { 164 | found = true 165 | } 166 | } 167 | if !found { 168 | accounts = append(accounts, account) 169 | } 170 | } 171 | } 172 | 173 | // build a list of names and lookup map 174 | aNames, aMap := MapAccounts(accounts) 175 | if len(aNames) == 0 { 176 | return fmt.Errorf("no accounts found") 177 | } 178 | 179 | // prompt user to select an account 180 | account, err := PromptSelect("Choose an Account:", aNames) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | // prompt user to select car 186 | carname, err := PromptSelect("Choose a Cloud Access Role:", aToCMap[account]) 187 | if err != nil { 188 | return err 189 | } 190 | 191 | // build enough of a car and return it 192 | car.Name = cMap[carname].CARName 193 | car.AccountName = aMap[account].Name 194 | car.AccountNumber = aMap[account].Number 195 | car.AccountID = aMap[account].ID 196 | car.AwsIamRoleName = cMap[carname].AwsIamRoleName 197 | car.AccountTypeID = aMap[account].TypeID 198 | car.ID = cMap[carname].CARID 199 | car.CloudAccessRoleType = cMap[carname].CARRoleType 200 | 201 | return nil 202 | } 203 | -------------------------------------------------------------------------------- /doc/man1/kion.1: -------------------------------------------------------------------------------- 1 | .\"Modified from man(1) of FreeBSD, the NetBSD mdoc.template, and mdoc.samples. 2 | .\"See Also: 3 | .\"man mdoc.samples for a complete listing of options 4 | .\"man mdoc for the short list of editing options 5 | .\"/usr/share/misc/mdoc.template 6 | .Dd 8/4/10 \" DATE 7 | .Dt KION 1 \" Program name and manual section number 8 | .Os Darwin 9 | .Sh NAME \" Section Header - required - don't modify 10 | .Nm kion 11 | .Nd Kion on the command line. 12 | .Sh SYNOPSIS \" Section Header - required - don't modify 13 | .Nm kion 14 | .Op Ar global options \" [global options] 15 | .Op Ar command \" [command] 16 | .Op Ar command options \" [command options] 17 | .Op Ar arg \" [arg] 18 | 19 | .Sh DESCRIPTION 20 | The Kion CLI allows users to perform common Kion workflows via the command line. Users can quickly generate short-term access keys (stak) or federate into the cloud service provider web console via configured favorites or by walking through an account and role selection wizard. 21 | 22 | 23 | .Sh GLOBAL OPTIONS 24 | .Bl -tag -width "-cloud-access-role" 25 | .It --endpoint URL, -e URL, --url URL 26 | URL of the Kion instance to interface with. 27 | .It --user USER, -u USER, --username USER 28 | Username used for authenticating with Kion. 29 | .It --password PASSWORD, -p PASSWORD 30 | Password used for authenticating with Kion. 31 | .It --idms IDMS_ID, -i IDMS_ID 32 | IDMS ID with which to authenticate if using username and password. 33 | .It --saml-metadata-file FILENAME|URL 34 | FILENAME or URL of the identity provider's XML metadata document. 35 | .It --saml-sp-issuer ISSUER 36 | SAML Service Provider issuer value from Kion. 37 | .It --saml-print-url 38 | Print the authentication URL instead of opening it automatically with the default browser. 39 | .It --token TOKEN, -t TOKEN 40 | Token (API or Bearer) used to authenticate. 41 | .It --disable-cache 42 | Disable the use of cache for Kion CLI. 43 | .It --debug 44 | Enable debug mode for additional CLI output. 45 | .It --quiet 46 | Enable quiet mode for to reduce unnecessary output. 47 | .It --profile PROFILE 48 | Use the specified PROFILE from the Kion CLI configuration file. 49 | .It --help, -h 50 | Print usage text. 51 | .It --version, -v 52 | Print the Kion CLI version. 53 | .El 54 | 55 | .Sh COMMANDS 56 | .Bl -tag -width "-cloud-access-role" 57 | .It stak, s 58 | Generate short-term access keys. 59 | .Bl -tag -width "-cloud-access-role" 60 | .It --print, -p 61 | Print STAK only. 62 | .It --account val, --acc val, -a val 63 | Target account number, must be passed with --car. 64 | .It --alias val, --aka val, -l val 65 | Target account alias, must be passed with --car. 66 | .It --car val, --cloud-access-role val, -c val 67 | Target cloud access role, must be passed with --account or --alias. 68 | .It --region val, -r val 69 | Specify which region to target. 70 | .It --save, -s 71 | Save short-term keys to an AWS credentials profile. 72 | .It --credential-process 73 | Setup Kion CLI as a credentials process subsystem. 74 | .It --help, -h 75 | Print usage text. 76 | .El 77 | 78 | .It favorite, fav, f 79 | Access pre-configured favorites. 80 | .Bl -tag -width "-cloud-access-role" 81 | .It list 82 | List all configured favorites. 83 | .It --print, -p 84 | Print STAK only. 85 | .It --access-type val, -t val 86 | Override a favorites access type. Expects "cli" or "web". 87 | .It --web, -w 88 | Shortcut for `--access-type web`. 89 | .It --credential-process 90 | Setup Kion CLI as a credentials process subsystem. 91 | .It --help, -h 92 | Print usage text. 93 | .El 94 | 95 | .It console, con, c 96 | Federate into the cloud service provider console. 97 | .Bl -tag -width "-cloud-access-role" 98 | .It --account val, --acc val, -a val 99 | Target account number, must be passed with --car. 100 | .It --alias val, --aka val, -l val 101 | Target account alias, must be passed with --car. 102 | .It --car val, --cloud-access-role val, -c val 103 | Target cloud access role, must be passed with --account or --alias. 104 | .It --help, -h 105 | Print usage text. 106 | .El 107 | 108 | .It run 109 | Run a command with short-term access keys. 110 | .Bl -tag -width "-cloud-access-role" 111 | .It --favorite val, --fav val, -f val 112 | Specify which favorite to run against. 113 | .It --account val, -acc val, -a val 114 | Specify which account to target, must be passed with --car. 115 | .It --alias val, --aka val, -l val 116 | Target account alias, must be passed with --car. 117 | .It --car val, -c val 118 | Specify which Cloud Access Role to use, must be passed with --account or --alias. 119 | .It --region val, -r val 120 | Specify which region to target. 121 | .It --help, -h 122 | Print usage text. 123 | .El 124 | 125 | .It util 126 | Tools for managing Kion CLI. 127 | .Bl -tag -width "push-favorites" 128 | .It flush-cache 129 | Clear out all cache entries for the Kion CLI. 130 | .It push-favorites 131 | Push locally defined favorites up to Kion. This will overwrite any favorites in Kion that have the same name. After pushing, you are prompted to delete local favorites. 132 | .It validate-saml 133 | Validate SAML configuration. 134 | .El 135 | 136 | .Sh PRECEDENCE 137 | Configuration settings are applied in the following order of precedence: 138 | .Bl -enum 139 | .It Flags 140 | Command-line flags have the highest precedence and will override any other settings. 141 | .It Environment Variables 142 | Environment variables override settings in the configuration file and default values. 143 | .It Configuration File 144 | Settings specified in the configuration file override default values. 145 | .It Default Values 146 | Default values are used when no other settings are provided. 147 | .El 148 | 149 | .Sh ENVIRONMENT VARIABLES 150 | .Bl -tag -width "KION_SAML_SP_ISSUER" 151 | .It KION_CONFIG 152 | Path to the Kion CLI configuration file. Defaults to ~/.kion.yml. 153 | .It KION_URL 154 | URL of the Kion instance to interact with. 155 | .It KION_USERNAME 156 | Username used for authenticating with Kion. 157 | .It KION_PASSWORD 158 | Password used for authenticating with Kion. 159 | .It KION_IDMS_ID 160 | IDMS ID with which to authenticate if using username and password. 161 | .It KION_API_KEY 162 | API key used to authenticate. 163 | .It KION_SAML_METADATA_FILE 164 | FILENAME or URL of the identity provider's XML metadata document. 165 | .It KION_SAML_SP_ISSUER 166 | The Kion IDMS issuer value. 167 | .It KION_SAML_PRINT_URL 168 | "TRUE" to print the authentication url as opposed to automatically opening it in the default browser. Defaults to "FALSE". 169 | .It KION_DEBUG 170 | "TRUE" to enable verbose debugging of the Kion CLI. 171 | .It KION_QUIET 172 | "TRUE" to reduce messages for quieter operation. 173 | .El 174 | 175 | .Sh FILES 176 | .Bl -tag -width "~/.kion.yml" 177 | .It Pa ~/.kion.yml 178 | The user configuration file. Defines credentials, target Kion instance, and a list of favorites. 179 | .El 180 | 181 | .Sh EXAMPLES 182 | .Bl -tag -width "kion console --account 111122223333 --car Admin" 183 | .It kion fav sandbox 184 | Open the sandbox AWS console favorited in the config. 185 | .It kion stak --print --account 121212121212 --car Admin 186 | Generate and print keys for an AWS account. 187 | .It kion console --account 111122223333 --car Admin 188 | Federate into a web console using an account number. 189 | .El 190 | 191 | .Sh SEE ALSO 192 | .Xr kion 1 193 | .Xr aws 1 194 | 195 | .\" .Sh BUGS \" Document known, unremedied bugs 196 | .\" .Sh HISTORY \" Document history if command behaves in a unique manner 197 | -------------------------------------------------------------------------------- /lib/helper/helper_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "github.com/kionsoftware/kion-cli/lib/kion" 5 | "github.com/kionsoftware/kion-cli/lib/structs" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | // // 10 | // Resources // 11 | // // 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | var kionTestProjects = []kion.Project{ 15 | {Archived: false, AutoPay: true, DefaultAwsRegion: "us-east-1", Description: "test description one", ID: 101, Name: "project one", OuID: 201}, 16 | {Archived: false, AutoPay: false, DefaultAwsRegion: "us-west-1", Description: "test description two", ID: 102, Name: "project two", OuID: 202}, 17 | {Archived: true, AutoPay: false, DefaultAwsRegion: "us-east-1", Description: "test description three", ID: 103, Name: "project three", OuID: 203}, 18 | {Archived: false, AutoPay: true, DefaultAwsRegion: "us-west-1", Description: "test description four", ID: 104, Name: "project four", OuID: 204}, 19 | {Archived: true, AutoPay: false, DefaultAwsRegion: "us-east-1", Description: "test description five", ID: 105, Name: "project five", OuID: 205}, 20 | {Archived: false, AutoPay: true, DefaultAwsRegion: "us-east-1", Description: "test description six", ID: 106, Name: "project six", OuID: 206}, 21 | } 22 | 23 | var kionTestProjectsNames = []string{ 24 | "project one", 25 | "project two", 26 | "project three", 27 | "project four", 28 | "project five", 29 | "project six", 30 | } 31 | 32 | var kionTestAccounts = []kion.Account{ 33 | {Email: "test1@kion.io", Name: "account one", Alias: "acct-one-alias", Number: "111111111111", TypeID: 1, ID: 101, IncludeLinkedAccountSpend: true, LinkedAccountNumber: "", LinkedRole: "", PayerID: 101, ProjectID: 101, SkipAccessChecking: true, UseOrgAccountInfo: false}, 34 | {Email: "test2@kion.io", Name: "account two", Alias: "acct-two-alias", Number: "121212121212", TypeID: 2, ID: 102, IncludeLinkedAccountSpend: false, LinkedAccountNumber: "", LinkedRole: "", PayerID: 102, ProjectID: 102, SkipAccessChecking: true, UseOrgAccountInfo: false}, 35 | {Email: "test3@kion.io", Name: "account three", Alias: "acct-three-alias", Number: "131313131313", TypeID: 3, ID: 103, IncludeLinkedAccountSpend: true, LinkedAccountNumber: "000000000000", LinkedRole: "", PayerID: 103, ProjectID: 103, SkipAccessChecking: true, UseOrgAccountInfo: false}, 36 | {Email: "test4@kion.io", Name: "account four", Alias: "acct-four-alias", Number: "141414141414", TypeID: 4, ID: 104, IncludeLinkedAccountSpend: false, LinkedAccountNumber: "", LinkedRole: "", PayerID: 104, ProjectID: 104, SkipAccessChecking: true, UseOrgAccountInfo: false}, 37 | {Email: "test5@kion.io", Name: "account five", Alias: "acct-five-alias", Number: "151515151515", TypeID: 5, ID: 105, IncludeLinkedAccountSpend: false, LinkedAccountNumber: "", LinkedRole: "", PayerID: 105, ProjectID: 105, SkipAccessChecking: true, UseOrgAccountInfo: false}, 38 | {Email: "test6@kion.io", Name: "account six", Alias: "acct-six-alias", Number: "161616161616", TypeID: 6, ID: 106, IncludeLinkedAccountSpend: false, LinkedAccountNumber: "", LinkedRole: "", PayerID: 106, ProjectID: 106, SkipAccessChecking: true, UseOrgAccountInfo: true}, 39 | } 40 | 41 | var kionTestAccountsNames = []string{ 42 | "account one", 43 | "account two", 44 | "account three", 45 | "account four", 46 | "account five", 47 | "account six", 48 | } 49 | 50 | var kionTestCARs = []kion.CAR{ 51 | {AccountID: 101, AccountNumber: "111111111111", AccountAlias: "acct-one-alias", AccountType: "aws", AccountTypeID: 1, AccountName: "account one", ApplyToAllAccounts: true, AwsIamPath: "some path", AwsIamRoleName: "role one", CloudAccessRoleType: "type", FutureAccounts: true, ID: 101, LongTermAccessKeys: false, Name: "car one", ProjectID: 101, ShortTermAccessKeys: true, WebAccess: true}, 52 | {AccountID: 102, AccountNumber: "121212121212", AccountAlias: "acct-two-alias", AccountType: "aws", AccountTypeID: 2, AccountName: "account two", ApplyToAllAccounts: true, AwsIamPath: "some path", AwsIamRoleName: "role two", CloudAccessRoleType: "type", FutureAccounts: true, ID: 102, LongTermAccessKeys: false, Name: "car two", ProjectID: 102, ShortTermAccessKeys: true, WebAccess: true}, 53 | {AccountID: 103, AccountNumber: "131313131313", AccountAlias: "acct-three-alias", AccountType: "aws", AccountTypeID: 3, AccountName: "account three", ApplyToAllAccounts: true, AwsIamPath: "some path", AwsIamRoleName: "role three", CloudAccessRoleType: "type", FutureAccounts: true, ID: 103, LongTermAccessKeys: false, Name: "car three", ProjectID: 103, ShortTermAccessKeys: true, WebAccess: true}, 54 | {AccountID: 104, AccountNumber: "141414141414", AccountAlias: "acct-four-alias", AccountType: "aws", AccountTypeID: 4, AccountName: "account four", ApplyToAllAccounts: true, AwsIamPath: "some path", AwsIamRoleName: "role four", CloudAccessRoleType: "type", FutureAccounts: true, ID: 104, LongTermAccessKeys: false, Name: "car four", ProjectID: 104, ShortTermAccessKeys: true, WebAccess: true}, 55 | {AccountID: 105, AccountNumber: "151515151515", AccountAlias: "acct-five-alias", AccountType: "aws", AccountTypeID: 5, AccountName: "account five", ApplyToAllAccounts: true, AwsIamPath: "some path", AwsIamRoleName: "role five", CloudAccessRoleType: "type", FutureAccounts: true, ID: 105, LongTermAccessKeys: false, Name: "car five", ProjectID: 105, ShortTermAccessKeys: true, WebAccess: true}, 56 | {AccountID: 106, AccountNumber: "161616161616", AccountAlias: "acct-six-alias", AccountType: "aws", AccountTypeID: 6, AccountName: "account six", ApplyToAllAccounts: true, AwsIamPath: "some path", AwsIamRoleName: "role six", CloudAccessRoleType: "type", FutureAccounts: true, ID: 106, LongTermAccessKeys: false, Name: "car six", ProjectID: 106, ShortTermAccessKeys: true, WebAccess: true}, 57 | } 58 | 59 | var kionTestCARsNames = []string{ 60 | "car one", 61 | "car two", 62 | "car three", 63 | "car four", 64 | "car five", 65 | "car six", 66 | } 67 | 68 | var kionTestIDMSs = []kion.IDMS{ 69 | {ID: 101, IdmsTypeID: 1, Name: "idms one"}, 70 | {ID: 102, IdmsTypeID: 2, Name: "idms two"}, 71 | {ID: 103, IdmsTypeID: 3, Name: "idms three"}, 72 | {ID: 104, IdmsTypeID: 4, Name: "idms four"}, 73 | {ID: 105, IdmsTypeID: 5, Name: "idms five"}, 74 | {ID: 106, IdmsTypeID: 6, Name: "idms six"}, 75 | } 76 | 77 | var kionTestIDMSsNames = []string{ 78 | "idms one", 79 | "idms two", 80 | "idms three", 81 | "idms four", 82 | "idms five", 83 | "idms six", 84 | } 85 | 86 | var kionTestFavorites = []structs.Favorite{ 87 | { 88 | Name: "fav one", 89 | Account: "111111111111", 90 | CAR: "car one", 91 | AccessType: "web", 92 | DescriptiveName: "fav one [local] (111111111111 car one web)", 93 | }, 94 | { 95 | Name: "fav two", 96 | Account: "121212121212", 97 | CAR: "car two", 98 | AccessType: "web", 99 | DescriptiveName: "fav two [local] (121212121212 car two web)", 100 | }, 101 | { 102 | Name: "fav three", 103 | Account: "131313131313", 104 | CAR: "car three", 105 | AccessType: "web", 106 | DescriptiveName: "fav three [local] (131313131313 car three web)", 107 | }, 108 | { 109 | Name: "fav four", 110 | Account: "141414141414", 111 | CAR: "car four", 112 | AccessType: "web", 113 | DescriptiveName: "fav four [local] (141414141414 car four web)", 114 | }, 115 | { 116 | Name: "fav five", 117 | Account: "151515151515", 118 | CAR: "car five", 119 | AccessType: "web", 120 | DescriptiveName: "fav five [local] (151515151515 car five web)", 121 | }, 122 | { 123 | Name: "fav six", 124 | Account: "161616161616", 125 | CAR: "car six", 126 | AccessType: "web", 127 | DescriptiveName: "fav six [local] (161616161616 car six web)", 128 | }, 129 | } 130 | 131 | //////////////////////////////////////////////////////////////////////////////// 132 | // // 133 | // Helpers // 134 | // // 135 | //////////////////////////////////////////////////////////////////////////////// 136 | -------------------------------------------------------------------------------- /lib/commands/authentication.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | "github.com/kionsoftware/kion-cli/lib/helper" 10 | "github.com/kionsoftware/kion-cli/lib/kion" 11 | samlTypes "github.com/russellhaering/gosaml2/types" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | // authUNPW prompts for any missing credentials then auths the users against 16 | // Kion, stores the session data, and sets the context token. 17 | func (c *Cmd) authUNPW(cCtx *cli.Context) error { 18 | var err error 19 | un := c.config.Kion.Username 20 | pw := c.config.Kion.Password 21 | idmsID := cCtx.Uint("idms") 22 | 23 | // prompt idms if needed 24 | if idmsID == 0 { 25 | idmss, err := kion.GetIDMSs(c.config.Kion.URL) 26 | if err != nil { 27 | return err 28 | } 29 | iNames, iMap := helper.MapIDMSs(idmss) 30 | if len(iNames) > 1 { 31 | idms, err := helper.PromptSelect("Select Login IDMS:", iNames) 32 | if err != nil { 33 | return err 34 | } 35 | idmsID = iMap[idms].ID 36 | } else { 37 | idmsID = iMap[iNames[0]].ID 38 | } 39 | } 40 | 41 | // prompt username if needed 42 | if un == "" { 43 | un, err = helper.PromptInput("Username:") 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | 49 | // prompt password if needed 50 | pwFoundInCache := false 51 | if pw == "" { 52 | // Check password cache 53 | pw, pwFoundInCache, err = c.cache.GetPassword(c.config.Kion.URL, idmsID, un) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | if !pwFoundInCache { 59 | pw, err = helper.PromptPassword("Password:") 60 | if err != nil { 61 | return err 62 | } 63 | } 64 | } 65 | 66 | // auth and capture our session 67 | session, err := kion.Authenticate(c.config.Kion.URL, idmsID, un, pw) 68 | if err != nil { 69 | // Unfortunately, the remote auth endpoint doesn't provide an easy way 70 | // of determining if an auth error was the cause of failure (it returns 71 | // an HTTP 400 with a body that contains a message about authentication 72 | // failues). Conservatively clear out any cached password when 73 | // Authenticate() fails 74 | if pwFoundInCache { 75 | err := c.cache.SetPassword(c.config.Kion.URL, idmsID, un, "") 76 | if err != nil { 77 | // We're already handling another error, logging 78 | // is the best we can do 79 | color.Red("Failed to clear password from cache, %v", err) 80 | } 81 | } 82 | return err 83 | } 84 | session.IDMSID = idmsID 85 | session.UserName = un 86 | err = c.cache.SetSession(session) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | // if auth succeeded, cache the password 92 | err = c.cache.SetPassword(c.config.Kion.URL, idmsID, un, pw) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | // set our token in the config 98 | c.config.Kion.APIKey = session.Access.Token 99 | return nil 100 | } 101 | 102 | // authSAML directs the user to authenticate via SAML in a web browser. 103 | // The SAML assertion is posted to this app which is forwarded to Kion and 104 | // exchanged for the context token. 105 | func (c *Cmd) authSAML(cCtx *cli.Context) error { 106 | var err error 107 | samlMetadataFile := c.config.Kion.SamlMetadataFile 108 | samlServiceProviderIssuer := c.config.Kion.SamlIssuer 109 | 110 | // Validate Kion URL is configured 111 | if c.config.Kion.URL == "" { 112 | return fmt.Errorf("the Kion URL is not configured; please set 'url' in your configuration file or use the --url flag") 113 | } 114 | 115 | // prompt metadata url if needed 116 | if samlMetadataFile == "" { 117 | samlMetadataFile, err = helper.PromptInput("SAML Metadata URL or File Path:") 118 | if err != nil { 119 | return err 120 | } 121 | if samlMetadataFile == "" { 122 | return fmt.Errorf("SAML Metadata URL/File is required for SAML authentication") 123 | } 124 | } 125 | 126 | // prompt issuer if needed 127 | if samlServiceProviderIssuer == "" { 128 | samlServiceProviderIssuer, err = helper.PromptInput("SAML Service Provider Issuer:") 129 | if err != nil { 130 | return err 131 | } 132 | if samlServiceProviderIssuer == "" { 133 | return fmt.Errorf("SAML Service Provider Issuer is required for SAML authentication") 134 | } 135 | } 136 | 137 | var samlMetadata *samlTypes.EntityDescriptor 138 | if strings.HasPrefix(samlMetadataFile, "http") { 139 | samlMetadata, err = kion.DownloadSAMLMetadata(samlMetadataFile) 140 | if err != nil { 141 | return fmt.Errorf("failed to download SAML metadata: %w", err) 142 | } 143 | } else { 144 | samlMetadata, err = kion.ReadSAMLMetadataFile(samlMetadataFile) 145 | if err != nil { 146 | return fmt.Errorf("failed to read SAML metadata file: %w", err) 147 | } 148 | } 149 | 150 | var authData *kion.AuthData 151 | 152 | // we only need to check for existence - the value is irrelevant 153 | if cCtx.App.Metadata["useOldSAML"] == true { 154 | authData, err = kion.AuthenticateSAMLOld( 155 | c.config.Kion.URL, 156 | samlMetadata, 157 | samlServiceProviderIssuer, 158 | c.config.Kion.SamlPrintURL, 159 | ) 160 | if err != nil { 161 | return err 162 | } 163 | } else { 164 | authData, err = kion.AuthenticateSAML( 165 | c.config.Kion.URL, 166 | samlMetadata, 167 | samlServiceProviderIssuer, 168 | c.config.Kion.SamlPrintURL, 169 | ) 170 | if err != nil { 171 | return err 172 | } 173 | } 174 | 175 | // cache the session for 9.5 minutes, tokens are valid for 10 minutes 176 | timeFormat := "2006-01-02T15:04:05-0700" 177 | session := kion.Session{ 178 | Access: struct { 179 | Expiry string `json:"expiry"` 180 | Token string `json:"token"` 181 | }{ 182 | Token: authData.AuthToken, 183 | Expiry: time.Now().Add(570 * time.Second).Format(timeFormat), 184 | }, 185 | } 186 | err = c.cache.SetSession(session) 187 | if err != nil { 188 | return err 189 | } 190 | 191 | // set our token in the config 192 | c.config.Kion.APIKey = authData.AuthToken 193 | return nil 194 | } 195 | 196 | // setAuthToken sets the token to be used for querying the Kion API. If not 197 | // passed to the tool as an argument, set in the env, or present in the 198 | // configuration dotfile it will prompt the users to authenticate. Auth methods 199 | // are prioritized as follows: api/bearer token -> username/password -> saml. 200 | // If flags are set for multiple methods the highest priority method will be 201 | // used. 202 | func (c *Cmd) setAuthToken(cCtx *cli.Context) error { 203 | if c.config.Kion.APIKey == "" { 204 | // if we still have an active session use it 205 | session, found, err := c.cache.GetSession() 206 | if err != nil { 207 | return err 208 | } 209 | if found && session.Access.Expiry != "" { 210 | timeFormat := "2006-01-02T15:04:05-0700" 211 | now := time.Now() 212 | expiration, err := time.Parse(timeFormat, session.Access.Expiry) 213 | if err != nil { 214 | return err 215 | } 216 | if expiration.After(now) { 217 | // TODO: test token is good with an endpoint that is accessible to all 218 | // user permission levels, if you get a 401 then assume token is bad 219 | // due to caching a cred when a users password expired, and flush the 220 | // cache instead... 221 | c.config.Kion.APIKey = session.Access.Token 222 | return nil 223 | } 224 | 225 | // TODO: uncomment when / if the application supports refresh tokens 226 | 227 | // // see if we can use the refresh token 228 | // refreshExp, err := time.Parse(timeFormat, session.Refresh.Expiry) 229 | // if err != nil { 230 | // return err 231 | // } 232 | 233 | // if refreshExp.After(now) { 234 | // un := session.UserName 235 | // idmsId := session.IDMSID 236 | // session, err = kion.Authenticate(c.config.Kion.Url, idmsId, un, session.Refresh.Token) 237 | // if err != nil { 238 | // return err 239 | // } 240 | // session.UserName = un 241 | // session.IDMSID = idmsId 242 | // err = c.cache.SetSession(session) 243 | // if err != nil { 244 | // return err 245 | // } 246 | 247 | // c.config.Kion.ApiKey = session.Access.Token 248 | // return nil 249 | // } 250 | } 251 | 252 | // check un / pw were set via flags and infer auth method 253 | if c.config.Kion.Username != "" || c.config.Kion.Password != "" { 254 | err := c.authUNPW(cCtx) 255 | return err 256 | } 257 | 258 | // check if saml auth flags set and auth with saml if so 259 | if c.config.Kion.SamlMetadataFile != "" && c.config.Kion.SamlIssuer != "" { 260 | err := c.authSAML(cCtx) 261 | return err 262 | } 263 | 264 | // if no token or session found, prompt for desired auth method 265 | methods := []string{ 266 | "API Key", 267 | "Password", 268 | "SAML", 269 | } 270 | authMethod, err := helper.PromptSelect("How would you like to authenticate", methods) 271 | if err != nil { 272 | return err 273 | } 274 | 275 | // handle chosen auth method 276 | switch authMethod { 277 | case "API Key": 278 | apiKey, err := helper.PromptPassword("API Key:") 279 | if err != nil { 280 | return err 281 | } 282 | c.config.Kion.APIKey = apiKey 283 | case "Password": 284 | err := c.authUNPW(cCtx) 285 | if err != nil { 286 | return err 287 | } 288 | case "SAML": 289 | err := c.authSAML(cCtx) 290 | if err != nil { 291 | return err 292 | } 293 | } 294 | } 295 | return nil 296 | } 297 | -------------------------------------------------------------------------------- /lib/helper/transform_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/kionsoftware/kion-cli/lib/kion" 9 | "github.com/kionsoftware/kion-cli/lib/structs" 10 | ) 11 | 12 | func TestMapProjects(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | projects []kion.Project 16 | wantOne []string 17 | wantTwo map[string]kion.Project 18 | }{ 19 | { 20 | "Basic", 21 | kionTestProjects, 22 | []string{ 23 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[4], kionTestProjects[4].ID), 24 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[3], kionTestProjects[3].ID), 25 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[0], kionTestProjects[0].ID), 26 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[5], kionTestProjects[5].ID), 27 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[2], kionTestProjects[2].ID), 28 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[1], kionTestProjects[1].ID), 29 | }, 30 | map[string]kion.Project{ 31 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[0], kionTestProjects[0].ID): kionTestProjects[0], 32 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[1], kionTestProjects[1].ID): kionTestProjects[1], 33 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[2], kionTestProjects[2].ID): kionTestProjects[2], 34 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[3], kionTestProjects[3].ID): kionTestProjects[3], 35 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[4], kionTestProjects[4].ID): kionTestProjects[4], 36 | fmt.Sprintf("%v (%v)", kionTestProjectsNames[5], kionTestProjects[5].ID): kionTestProjects[5], 37 | }, 38 | }, 39 | } 40 | 41 | for _, test := range tests { 42 | t.Run(test.name, func(t *testing.T) { 43 | one, two := MapProjects(test.projects) 44 | if !reflect.DeepEqual(test.wantOne, one) || !reflect.DeepEqual(test.wantTwo, two) { 45 | t.Errorf("\ngot:\n %v\n %v\nwanted:\n %v\n %v", one, two, test.wantOne, test.wantTwo) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestMapAccounts(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | accounts []kion.Account 55 | wantOne []string 56 | wantTwo map[string]kion.Account 57 | }{ 58 | { 59 | "Basic", 60 | kionTestAccounts, 61 | []string{ 62 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[4], kionTestAccounts[4].Alias, kionTestAccounts[4].Number), 63 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[3], kionTestAccounts[3].Alias, kionTestAccounts[3].Number), 64 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[0], kionTestAccounts[0].Alias, kionTestAccounts[0].Number), 65 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[5], kionTestAccounts[5].Alias, kionTestAccounts[5].Number), 66 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[2], kionTestAccounts[2].Alias, kionTestAccounts[2].Number), 67 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[1], kionTestAccounts[1].Alias, kionTestAccounts[1].Number), 68 | }, 69 | map[string]kion.Account{ 70 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[0], kionTestAccounts[0].Alias, kionTestAccounts[0].Number): kionTestAccounts[0], 71 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[1], kionTestAccounts[1].Alias, kionTestAccounts[1].Number): kionTestAccounts[1], 72 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[2], kionTestAccounts[2].Alias, kionTestAccounts[2].Number): kionTestAccounts[2], 73 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[3], kionTestAccounts[3].Alias, kionTestAccounts[3].Number): kionTestAccounts[3], 74 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[4], kionTestAccounts[4].Alias, kionTestAccounts[4].Number): kionTestAccounts[4], 75 | fmt.Sprintf("%v [%v] (%v)", kionTestAccountsNames[5], kionTestAccounts[5].Alias, kionTestAccounts[5].Number): kionTestAccounts[5], 76 | }, 77 | }, 78 | } 79 | 80 | for _, test := range tests { 81 | t.Run(test.name, func(t *testing.T) { 82 | one, two := MapAccounts(test.accounts) 83 | if !reflect.DeepEqual(test.wantOne, one) || !reflect.DeepEqual(test.wantTwo, two) { 84 | t.Errorf("\ngot:\n %v\n %v\nwanted:\n %v\n %v", one, two, test.wantOne, test.wantTwo) 85 | } 86 | }) 87 | } 88 | } 89 | func TestMapCAR(t *testing.T) { 90 | tests := []struct { 91 | name string 92 | cars []kion.CAR 93 | wantOne []string 94 | wantTwo map[string]kion.CAR 95 | }{ 96 | { 97 | "Basic", 98 | kionTestCARs, 99 | []string{ 100 | fmt.Sprintf("%v (%v)", kionTestCARsNames[4], kionTestCARs[4].ID), 101 | fmt.Sprintf("%v (%v)", kionTestCARsNames[3], kionTestCARs[3].ID), 102 | fmt.Sprintf("%v (%v)", kionTestCARsNames[0], kionTestCARs[0].ID), 103 | fmt.Sprintf("%v (%v)", kionTestCARsNames[5], kionTestCARs[5].ID), 104 | fmt.Sprintf("%v (%v)", kionTestCARsNames[2], kionTestCARs[2].ID), 105 | fmt.Sprintf("%v (%v)", kionTestCARsNames[1], kionTestCARs[1].ID), 106 | }, 107 | map[string]kion.CAR{ 108 | fmt.Sprintf("%v (%v)", kionTestCARsNames[0], kionTestCARs[0].ID): kionTestCARs[0], 109 | fmt.Sprintf("%v (%v)", kionTestCARsNames[1], kionTestCARs[1].ID): kionTestCARs[1], 110 | fmt.Sprintf("%v (%v)", kionTestCARsNames[2], kionTestCARs[2].ID): kionTestCARs[2], 111 | fmt.Sprintf("%v (%v)", kionTestCARsNames[3], kionTestCARs[3].ID): kionTestCARs[3], 112 | fmt.Sprintf("%v (%v)", kionTestCARsNames[4], kionTestCARs[4].ID): kionTestCARs[4], 113 | fmt.Sprintf("%v (%v)", kionTestCARsNames[5], kionTestCARs[5].ID): kionTestCARs[5], 114 | }, 115 | }, 116 | } 117 | 118 | for _, test := range tests { 119 | t.Run(test.name, func(t *testing.T) { 120 | one, two := MapCAR(test.cars) 121 | if !reflect.DeepEqual(test.wantOne, one) || !reflect.DeepEqual(test.wantTwo, two) { 122 | t.Errorf("\ngot:\n %v\n %v\nwanted:\n %v\n %v", one, two, test.wantOne, test.wantTwo) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func TestMapIDMSs(t *testing.T) { 129 | tests := []struct { 130 | name string 131 | idmss []kion.IDMS 132 | wantOne []string 133 | wantTwo map[string]kion.IDMS 134 | }{ 135 | { 136 | "Basic", 137 | kionTestIDMSs, 138 | []string{ 139 | kionTestIDMSsNames[4], 140 | kionTestIDMSsNames[3], 141 | kionTestIDMSsNames[0], 142 | kionTestIDMSsNames[5], 143 | kionTestIDMSsNames[2], 144 | kionTestIDMSsNames[1], 145 | }, 146 | map[string]kion.IDMS{ 147 | kionTestIDMSsNames[0]: kionTestIDMSs[0], 148 | kionTestIDMSsNames[1]: kionTestIDMSs[1], 149 | kionTestIDMSsNames[2]: kionTestIDMSs[2], 150 | kionTestIDMSsNames[3]: kionTestIDMSs[3], 151 | kionTestIDMSsNames[4]: kionTestIDMSs[4], 152 | kionTestIDMSsNames[5]: kionTestIDMSs[5], 153 | }, 154 | }, 155 | } 156 | 157 | for _, test := range tests { 158 | t.Run(test.name, func(t *testing.T) { 159 | one, two := MapIDMSs(test.idmss) 160 | // if !reflect.DeepEqual(test.wantOne, one) || !reflect.DeepEqual(test.wantTwo, two) { 161 | if !reflect.DeepEqual(test.wantOne, one) || !reflect.DeepEqual(test.wantTwo, two) { 162 | t.Errorf("\ngot:\n %v\n %v\nwanted:\n %v\n %v", one, two, test.wantOne, test.wantTwo) 163 | } 164 | }) 165 | } 166 | } 167 | 168 | func TestMapFavs(t *testing.T) { 169 | tests := []struct { 170 | name string 171 | favorites []structs.Favorite 172 | wantOne []string 173 | wantTwo map[string]structs.Favorite 174 | }{ 175 | { 176 | "Basic", 177 | kionTestFavorites, 178 | []string{ 179 | "fav five [local] (151515151515 car five web)", 180 | "fav four [local] (141414141414 car four web)", 181 | "fav one [local] (111111111111 car one web)", 182 | "fav six [local] (161616161616 car six web)", 183 | "fav three [local] (131313131313 car three web)", 184 | "fav two [local] (121212121212 car two web)", 185 | }, 186 | map[string]structs.Favorite{ 187 | "fav one [local] (111111111111 car one web)": kionTestFavorites[0], 188 | "fav two [local] (121212121212 car two web)": kionTestFavorites[1], 189 | "fav three [local] (131313131313 car three web)": kionTestFavorites[2], 190 | "fav four [local] (141414141414 car four web)": kionTestFavorites[3], 191 | "fav five [local] (151515151515 car five web)": kionTestFavorites[4], 192 | "fav six [local] (161616161616 car six web)": kionTestFavorites[5], 193 | }, 194 | }, 195 | } 196 | 197 | for _, test := range tests { 198 | t.Run(test.name, func(t *testing.T) { 199 | one, two := MapFavs(test.favorites) 200 | if !reflect.DeepEqual(test.wantOne, one) || !reflect.DeepEqual(test.wantTwo, two) { 201 | t.Errorf("\ngot:\n %v\n %v\nwanted:\n %v\n %v", one, two, test.wantOne, test.wantTwo) 202 | } 203 | }) 204 | } 205 | } 206 | 207 | func TestFindCARByName(t *testing.T) { 208 | tests := []struct { 209 | name string 210 | find string 211 | cars []kion.CAR 212 | wantCAR kion.CAR 213 | wantErr error 214 | }{ 215 | { 216 | "Find Match", 217 | "car one", 218 | kionTestCARs, 219 | kionTestCARs[0], 220 | nil, 221 | }, 222 | { 223 | "Find No Match", 224 | "fake car", 225 | kionTestCARs, 226 | kion.CAR{}, 227 | fmt.Errorf("cannot find cloud access role with name %v", "fake car"), 228 | }, 229 | } 230 | 231 | for _, test := range tests { 232 | t.Run(test.name, func(t *testing.T) { 233 | car, err := FindCARByName(test.cars, test.find) 234 | // if !reflect.DeepEqual(&test.wantCAR, car) || test.wantErr != err { 235 | if !reflect.DeepEqual(&test.wantCAR, car) || !reflect.DeepEqual(test.wantErr, err) { 236 | t.Errorf("\ngot:\n %v\n %v\nwanted:\n %v\n %v", car, err, &test.wantCAR, test.wantErr) 237 | } 238 | }) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /lib/helper/transform.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "sort" 7 | 8 | "github.com/kionsoftware/kion-cli/lib/kion" 9 | "github.com/kionsoftware/kion-cli/lib/structs" 10 | 11 | "github.com/fatih/color" 12 | ) 13 | 14 | func padName(name string) string { 15 | nameLen := len(name) 16 | padding := fmt.Sprintf("%*s", max(12-nameLen, 0), "") 17 | return fmt.Sprintf("%s%s", name, padding) 18 | } 19 | 20 | //////////////////////////////////////////////////////////////////////////////// 21 | // // 22 | // Transform // 23 | // // 24 | //////////////////////////////////////////////////////////////////////////////// 25 | 26 | // MapProjects transforms a slice of Projects into a slice of their names and a 27 | // map indexed by their names. 28 | func MapProjects(projects []kion.Project) ([]string, map[string]kion.Project) { 29 | var pNames []string 30 | pMap := make(map[string]kion.Project) 31 | for _, project := range projects { 32 | name := fmt.Sprintf("%v (%v)", project.Name, project.ID) 33 | pNames = append(pNames, name) 34 | pMap[name] = project 35 | } 36 | sort.Strings(pNames) 37 | 38 | return pNames, pMap 39 | } 40 | 41 | // MapAccounts transforms a slice of Accounts into a slice of their names and a 42 | // map indexed by their names. 43 | func MapAccounts(accounts []kion.Account) ([]string, map[string]kion.Account) { 44 | var aNames []string 45 | aMap := make(map[string]kion.Account) 46 | for _, account := range accounts { 47 | var name string 48 | if account.Alias != "" { 49 | name = fmt.Sprintf("%v [%v] (%v)", account.Name, account.Alias, account.Number) 50 | } else { 51 | name = fmt.Sprintf("%v (%v)", account.Name, account.Number) 52 | } 53 | aNames = append(aNames, name) 54 | aMap[name] = account 55 | } 56 | sort.Strings(aNames) 57 | 58 | return aNames, aMap 59 | } 60 | 61 | // MapAccountsFromCARS transforms a slice of CARs into a slice of account names 62 | // and a map of account numbers indexed by their names. If a project ID is 63 | // passed it will only return accounts in the given project. Note that some 64 | // versions of Kion will not populate account metadata in CAR objects so use 65 | // carefully (see useUpdatedCloudAccessRoleAPI bool). 66 | func MapAccountsFromCARS(cars []kion.CAR, pid uint) ([]string, map[string]string) { 67 | var aNames []string 68 | aMap := make(map[string]string) 69 | for _, car := range cars { 70 | if pid == 0 || car.ProjectID == pid { 71 | var name string 72 | if car.AccountAlias != "" { 73 | name = fmt.Sprintf("%v [%v] (%v)", car.AccountName, car.AccountAlias, car.AccountNumber) 74 | } else { 75 | name = fmt.Sprintf("%v (%v)", car.AccountName, car.AccountNumber) 76 | } 77 | if slices.Contains(aNames, name) { 78 | continue 79 | } 80 | aNames = append(aNames, name) 81 | aMap[name] = car.AccountNumber 82 | } 83 | } 84 | sort.Strings(aNames) 85 | 86 | return aNames, aMap 87 | } 88 | 89 | // MapCAR transforms a slice of CARs into a slice of their names and a map 90 | // indexed by their names. 91 | func MapCAR(cars []kion.CAR) ([]string, map[string]kion.CAR) { 92 | var cNames []string 93 | cMap := make(map[string]kion.CAR) 94 | for _, car := range cars { 95 | name := fmt.Sprintf("%v (%v)", car.Name, car.ID) 96 | cNames = append(cNames, name) 97 | cMap[name] = car 98 | } 99 | sort.Strings(cNames) 100 | 101 | return cNames, cMap 102 | } 103 | 104 | // MapIDMSs transforms a slice of IDMSs into a slice of their names and a map 105 | // indexed by their names. 106 | func MapIDMSs(idmss []kion.IDMS) ([]string, map[string]kion.IDMS) { 107 | var iNames []string 108 | iMap := make(map[string]kion.IDMS) 109 | for _, idms := range idmss { 110 | iNames = append(iNames, idms.Name) 111 | iMap[idms.Name] = idms 112 | } 113 | sort.Strings(iNames) 114 | 115 | return iNames, iMap 116 | } 117 | 118 | // MapFavs transforms a slice of Favorites into a slice of their names and a 119 | // map indexed by their names. 120 | func MapFavs(favs []structs.Favorite) ([]string, map[string]structs.Favorite) { 121 | var fNames []string 122 | fMap := make(map[string]structs.Favorite) 123 | for _, fav := range favs { 124 | fNames = append(fNames, fav.DescriptiveName) 125 | fMap[fav.DescriptiveName] = fav 126 | } 127 | sort.Strings(fNames) 128 | 129 | return fNames, fMap 130 | } 131 | 132 | // FindCARByName returns a CAR identified by its name. 133 | func FindCARByName(cars []kion.CAR, carName string) (*kion.CAR, error) { 134 | for _, c := range cars { 135 | if c.Name == carName { 136 | return &c, nil 137 | } 138 | } 139 | return &kion.CAR{}, fmt.Errorf("cannot find cloud access role with name %v", carName) 140 | } 141 | 142 | // CombineFavorites combines local favorites with API favorites, identifying 143 | // exact matches, unaliased matches, conflicts, and local-only favorites. It 144 | // returns a combined slice of all favorites and a detailed comparison struct. 145 | func CombineFavorites(localFavs []structs.Favorite, upstreamFavs []structs.Favorite) ([]structs.Favorite, *structs.FavoritesComparison, error) { 146 | 147 | result := structs.FavoritesComparison{} 148 | upstreamConflictsSeen := make(map[string]bool) 149 | 150 | for _, fav := range upstreamFavs { 151 | // Include metadata with name 152 | fav.DescriptiveName = fmt.Sprintf("%s %s %s", 153 | padName(fav.Name), 154 | color.GreenString("[Kion] "), 155 | color.New(color.Faint).Sprintf("(%s %s %s)", fav.Account, fav.CAR, fav.AccessType), 156 | ) 157 | 158 | result.All = append(result.All, fav) 159 | } 160 | 161 | for _, localFav := range localFavs { 162 | localKey := fmt.Sprintf("%s|%s|%s|%s", localFav.Name, localFav.Account, localFav.CAR, localFav.AccessType) 163 | localUnaliased := fmt.Sprintf("%s|%s|%s", localFav.Account, localFav.CAR, localFav.AccessType) 164 | foundMatch := false 165 | 166 | for _, upstreamFav := range upstreamFavs { 167 | // upstreamFav.AccessType = kion.ConvertAccessType(upstreamFav.AccessType) 168 | upstreamKey := fmt.Sprintf("%s|%s|%s|%s", upstreamFav.Name, upstreamFav.Account, upstreamFav.CAR, upstreamFav.AccessType) 169 | upstreamUnaliased := fmt.Sprintf("%s|%s|%s", upstreamFav.Account, upstreamFav.CAR, upstreamFav.AccessType) 170 | 171 | // Exact match 172 | if upstreamKey == localKey { 173 | foundMatch = true 174 | break 175 | } 176 | 177 | // Name conflict (two with same name but different 178 | // account/CAR/AccessType) 179 | if upstreamFav.Name == localFav.Name && !upstreamFav.Unaliased { 180 | result.ConflictsLocal = append(result.ConflictsLocal, localFav) 181 | if !upstreamConflictsSeen[upstreamKey] { 182 | result.ConflictsUpstream = append(result.ConflictsUpstream, upstreamFav) 183 | upstreamConflictsSeen[upstreamKey] = true 184 | } 185 | localFav.DescriptiveName = fmt.Sprintf("%s %s %s %s", 186 | padName(localFav.Name), 187 | color.New(color.FgBlue).Sprintf("[local]"), 188 | color.New(color.Faint).Sprintf("(%s %s %s)", localFav.Account, localFav.CAR, localFav.AccessType), 189 | color.New(color.FgRed).Sprintf("conflicts w/ %s", upstreamFav.Name), 190 | ) 191 | result.All = append(result.All, localFav) 192 | foundMatch = true 193 | break 194 | } 195 | 196 | // Name conflict (different name but same 197 | // account/CAR/AccessType) 198 | if upstreamUnaliased == localUnaliased && !upstreamFav.Unaliased { 199 | result.ConflictsLocal = append(result.ConflictsLocal, localFav) 200 | if !upstreamConflictsSeen[upstreamKey] { 201 | result.ConflictsUpstream = append(result.ConflictsUpstream, upstreamFav) 202 | upstreamConflictsSeen[upstreamKey] = true 203 | } 204 | localFav.DescriptiveName = fmt.Sprintf("%s %s %s %s", 205 | padName(localFav.Name), 206 | color.New(color.FgBlue).Sprintf("[local]"), 207 | color.New(color.Faint).Sprintf("(%s %s %s)", localFav.Account, localFav.CAR, localFav.AccessType), 208 | color.New(color.FgYellow).Sprintf("duplicate of %s", upstreamFav.Name), 209 | ) 210 | result.All = append(result.All, localFav) 211 | foundMatch = true 212 | break 213 | } 214 | 215 | // Account + CAR + AccessType match (no name) 216 | if upstreamUnaliased == localUnaliased && upstreamFav.Unaliased { 217 | result.UnaliasedLocal = append(result.UnaliasedLocal, localFav) 218 | result.UnaliasedUpstream = append(result.UnaliasedUpstream, upstreamFav) 219 | // Remove upstreamFav from result.All 220 | for i, fav := range result.All { 221 | if fav.Account == upstreamFav.Account && fav.CAR == upstreamFav.CAR && 222 | fav.AccessType == upstreamFav.AccessType && fav.Name == upstreamFav.Name { 223 | result.All = append(result.All[:i], result.All[i+1:]...) 224 | break 225 | } 226 | } 227 | localFav.DescriptiveName = fmt.Sprintf("%s %s %s %s", 228 | padName(localFav.Name), 229 | color.New(color.FgBlue).Sprintf("[local]"), 230 | color.New(color.Faint).Sprintf("(%s %s %s)", localFav.Account, localFav.CAR, localFav.AccessType), 231 | color.New(color.FgYellow).Sprintf("duplicate of unaliased"), 232 | ) 233 | result.All = append(result.All, localFav) 234 | foundMatch = true 235 | break 236 | } 237 | } 238 | 239 | if !foundMatch { 240 | result.LocalOnly = append(result.LocalOnly, localFav) 241 | localFav.DescriptiveName = fmt.Sprintf("%s %s %s", 242 | padName(localFav.Name), 243 | color.New(color.FgBlue).Sprintf("[local]"), 244 | color.New(color.Faint).Sprintf("(%s %s %s)", localFav.Account, localFav.CAR, localFav.AccessType), 245 | ) 246 | result.All = append(result.All, localFav) 247 | } 248 | } 249 | 250 | return result.All, &result, nil 251 | } 252 | -------------------------------------------------------------------------------- /lib/commands/utility.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | "github.com/kionsoftware/kion-cli/lib/helper" 10 | "github.com/kionsoftware/kion-cli/lib/kion" 11 | "github.com/kionsoftware/kion-cli/lib/structs" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | //////////////////////////////////////////////////////////////////////////////// 16 | // // 17 | // Helpers // 18 | // // 19 | //////////////////////////////////////////////////////////////////////////////// 20 | 21 | // deleteUpstreamFavorites deletes favorites in the Kion API. It assumes you are 22 | // passing upstream defined favorites only as we don't want to delete local 23 | // only favorites. 24 | func (c *Cmd) deleteUpstreamFavorites(favorites []structs.Favorite) error { 25 | hasErrors := false 26 | for _, f := range favorites { 27 | fmt.Printf(" removing upstream favorite %s: ", f.Name) 28 | _, err := kion.DeleteFavorite(c.config.Kion.URL, c.config.Kion.APIKey, f.Name) 29 | if err != nil { 30 | color.Red("x %s\n", err) 31 | hasErrors = true 32 | continue 33 | } 34 | color.Green("✓") 35 | } 36 | if hasErrors { 37 | return errors.New("one or more favorites failed to delete") 38 | } 39 | return nil 40 | } 41 | 42 | // createUpstreamFavorite creates favorites in the Kion API. It assumes you are 43 | // passing locally defined favorites only as we convert the access type from 44 | // cli to api format. 45 | func (c *Cmd) createUpstreamFavorite(favorites []structs.Favorite) error { 46 | hasErrors := false 47 | for _, f := range favorites { 48 | fmt.Printf(" creating favorite %s: ", f.Name) 49 | f.AccessType = kion.ConvertAccessType(f.AccessType) 50 | _, _, err := kion.CreateFavorite(c.config.Kion.URL, c.config.Kion.APIKey, f) 51 | if err != nil { 52 | color.Red("x %s\n", err) 53 | hasErrors = true 54 | continue 55 | } 56 | color.Green("✓") 57 | } 58 | if hasErrors { 59 | return errors.New("one or more errors occurred during the creation process") 60 | } 61 | return nil 62 | } 63 | 64 | //////////////////////////////////////////////////////////////////////////////// 65 | // // 66 | // Commands // 67 | // // 68 | //////////////////////////////////////////////////////////////////////////////// 69 | 70 | // ValidateSAML validates SAML configuration and connectivity. 71 | func (c *Cmd) ValidateSAML(cCtx *cli.Context) error { 72 | ctx := newValidationContext() 73 | 74 | // Header 75 | fmt.Println() 76 | fmt.Println(ctx.styles.renderMainHeader("SAML Configuration Validation")) 77 | fmt.Println(ctx.styles.renderSeparator()) 78 | fmt.Println() 79 | 80 | // Check basic configuration 81 | if err := c.checkBasicConfig(ctx); err != nil { 82 | return err 83 | } 84 | 85 | // Check port availability 86 | c.checkPortAvailability(ctx) 87 | 88 | // Check Kion connectivity 89 | kionAccessible := c.checkKionConnectivity(ctx) 90 | 91 | // Load and validate metadata 92 | metadata, err := c.loadMetadata(ctx) 93 | if err == nil { 94 | // Validate metadata structure 95 | if c.validateMetadataStructure(ctx, metadata) { 96 | // Validate certificates 97 | c.validateCertificates(ctx, metadata) 98 | 99 | // Check SSO URL reachability 100 | c.checkSSOURLReachability(ctx, metadata) 101 | } 102 | } 103 | fmt.Println() 104 | 105 | // Check CSRF endpoint if Kion is accessible 106 | if kionAccessible { 107 | c.checkCSRFEndpoint(ctx) 108 | } 109 | 110 | // Summary 111 | fmt.Println(ctx.styles.renderSeparator()) 112 | if ctx.allPassed { 113 | var summary strings.Builder 114 | summary.WriteString("✓ All validation checks passed!\n\n") 115 | summary.WriteString("Your SAML configuration appears to be correct.\n") 116 | summary.WriteString("Try running SAML authentication to complete the flow.") 117 | 118 | successBox := ctx.styles.summaryBox.BorderForeground(ctx.styles.checkMark.GetForeground()) 119 | fmt.Println(successBox.Render(summary.String())) 120 | 121 | // Print metadata details after success message 122 | if metadata != nil { 123 | c.printMetadataDetails(ctx, metadata) 124 | } 125 | return nil 126 | } 127 | 128 | var summary strings.Builder 129 | summary.WriteString("✗ Some validation checks failed.\n\n") 130 | summary.WriteString("Please review the errors above and fix the configuration.") 131 | 132 | failBox := ctx.styles.summaryBox.BorderForeground(ctx.styles.xMark.GetForeground()) 133 | fmt.Println(failBox.Render(summary.String())) 134 | return fmt.Errorf("SAML validation failed") 135 | } 136 | 137 | // FlushCache clears the Kion CLI cache. 138 | func (c *Cmd) FlushCache(cCtx *cli.Context) error { 139 | return c.cache.FlushCache() 140 | } 141 | 142 | // PushFavorites pushes the local favorites to a target instance of Kion. 143 | func (c *Cmd) PushFavorites(cCtx *cli.Context) error { 144 | // Exit if not using a compatible Kion version. 145 | if !cCtx.App.Metadata["useFavoritesAPI"].(bool) { 146 | err := errors.New("favorites API is not enabled. This requires Kion version 3.13.5, 3.14.1 or higher") 147 | return err 148 | } 149 | 150 | // Exit if no local favorites are defined. 151 | if len(c.config.Favorites) == 0 { 152 | color.Yellow("No local favorites found for the current profile. Nothing to push.") 153 | return nil 154 | } 155 | 156 | // Track errors during the push process. This will be used to determine if 157 | // we should delete local favorites after the push. 158 | var hasErrors bool 159 | 160 | // Authenticate before making API calls 161 | err := c.setAuthToken(cCtx) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | // Get the combined list of favorites from the CLI config and the Kion API. 167 | apiFavorites, _, err := kion.GetAPIFavorites(c.config.Kion.URL, c.config.Kion.APIKey) 168 | if err != nil { 169 | fmt.Printf("Error retrieving favorites from Kion API: %v\n", err) 170 | return err 171 | } 172 | _, favorites, err := helper.CombineFavorites(c.config.Favorites, apiFavorites) 173 | if err != nil { 174 | fmt.Printf("Error combining favorites: %v\n", err) 175 | return err 176 | } 177 | 178 | // Check if there's anything to push. 179 | changes := len(favorites.LocalOnly) + len(favorites.ConflictsLocal) + len(favorites.UnaliasedLocal) 180 | if changes == 0 { 181 | color.Green("All local favorites are already uploaded to Kion.\n") 182 | return nil 183 | } 184 | 185 | // Build the prompt message. 186 | prompt := fmt.Sprintf("\nThe following local favorites will be pushed to Kion (%v):\n\n", c.config.Kion.URL) 187 | for _, f := range favorites.LocalOnly { 188 | prompt += fmt.Sprintf(" - %s %s\n", f.Name, color.GreenString("(new)")) 189 | } 190 | for _, f := range favorites.ConflictsLocal { 191 | prompt += fmt.Sprintf(" - %s %s\n", f.Name, color.RedString("(upstream conflict)")) 192 | } 193 | for _, f := range favorites.UnaliasedLocal { 194 | prompt += fmt.Sprintf(" - %s %s\n", f.Name, color.YellowString("(will update alias on existing favorite)")) 195 | } 196 | if len(favorites.ConflictsLocal) > 0 { 197 | prompt += fmt.Sprintf("%s\n", color.RedString("\nPushing local favorites with conflicts will overwrite upstream Kion favorites!")) 198 | } 199 | prompt += "\nDo you want to continue?" 200 | 201 | // Confirm the push. 202 | selection, err := helper.PromptSelect(prompt, []string{"no", "yes"}) 203 | if selection == "no" || err != nil { 204 | fmt.Println("\nAborting push of favorites.") 205 | return err 206 | } 207 | if len(favorites.ConflictsLocal) > 0 { 208 | confirm, err := helper.PromptSelect( 209 | "\nConflicting favorites in Kion will be overwritten, are you sure you want to continue?", 210 | []string{"no", "yes"}, 211 | ) 212 | if confirm == "no" || err != nil { 213 | fmt.Println("\nAborting push of favorites due to conflicts.") 214 | return err 215 | } 216 | } 217 | 218 | // Push new local-only favorites. 219 | err = c.createUpstreamFavorite(favorites.LocalOnly) 220 | if err != nil { 221 | hasErrors = true 222 | } 223 | 224 | // Handle conflicts by deleting and recreating. 225 | err = c.deleteUpstreamFavorites(favorites.ConflictsUpstream) 226 | if err != nil { 227 | hasErrors = true 228 | } 229 | err = c.createUpstreamFavorite(favorites.ConflictsLocal) 230 | if err != nil { 231 | hasErrors = true 232 | } 233 | 234 | // Handle unaliased favorites (create will overwrite / update). 235 | err = c.createUpstreamFavorite(favorites.UnaliasedLocal) 236 | if err != nil { 237 | hasErrors = true 238 | } 239 | 240 | // Remove local favorites after successful push. 241 | if !hasErrors { 242 | return c.DeleteLocalFavorites(cCtx) 243 | } else { 244 | return errors.New("one or more errors occurred, local favorites have not been deleted") 245 | } 246 | } 247 | 248 | func (c *Cmd) DeleteLocalFavorites(cCtx *cli.Context) error { 249 | confirmDelete, err := helper.PromptSelect("\nDo you want to delete the local favorites?", []string{"no", "yes"}) 250 | if err != nil { 251 | color.Red("Error prompting for deletion confirmation: %v\n", err) 252 | return err 253 | } 254 | if confirmDelete == "yes" { 255 | 256 | configPath := cCtx.App.Metadata["configPath"].(string) 257 | 258 | // load the full config file 259 | var config structs.Configuration 260 | err := helper.LoadConfig(configPath, &config) 261 | if err != nil { 262 | color.Red("Error loading config: %v\n", err) 263 | return err 264 | } 265 | 266 | // if using a profile, delete favorites from that profile 267 | // otherwise delete favorites from the default profile 268 | profile := cCtx.String("profile") 269 | if profile == "" { 270 | config.Favorites = []structs.Favorite{} 271 | } else { 272 | profileConfig := config.Profiles[profile] 273 | profileConfig.Favorites = []structs.Favorite{} 274 | config.Profiles[profile] = profileConfig 275 | } 276 | 277 | // Save the updated config back to the file 278 | err = helper.SaveConfig(configPath, config) 279 | if err != nil { 280 | color.Red("Error saving updated config: %v\n", err) 281 | return err 282 | } 283 | color.Green("\nLocal favorites deleted after successful push to Kion API.\n") 284 | } else { 285 | color.Green("\nKeeping local favorites.\n") 286 | } 287 | 288 | return nil 289 | } 290 | -------------------------------------------------------------------------------- /lib/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Package commands provides the command implementations for the Kion CLI tool. 2 | package commands 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "github.com/99designs/keyring" 9 | "github.com/hashicorp/go-version" 10 | "github.com/kionsoftware/kion-cli/lib/cache" 11 | "github.com/kionsoftware/kion-cli/lib/helper" 12 | "github.com/kionsoftware/kion-cli/lib/kion" 13 | "github.com/kionsoftware/kion-cli/lib/structs" 14 | 15 | "github.com/urfave/cli/v2" 16 | ) 17 | 18 | //////////////////////////////////////////////////////////////////////////////// 19 | // // 20 | // Commands Object // 21 | // // 22 | //////////////////////////////////////////////////////////////////////////////// 23 | 24 | // Cmd is the main command object for the Kion CLI. It makes the configuration 25 | // and cache available to all command actions. 26 | type Cmd struct { 27 | config *structs.Configuration 28 | cache cache.Cache 29 | } 30 | 31 | // NewCommands stands up a new instance of commands with the provided 32 | // configuration. 33 | func NewCommands(cfg *structs.Configuration) *Cmd { 34 | return &Cmd{ 35 | config: cfg, 36 | } 37 | } 38 | 39 | //////////////////////////////////////////////////////////////////////////////// 40 | // // 41 | // Helpers // 42 | // // 43 | //////////////////////////////////////////////////////////////////////////////// 44 | 45 | // getSecondArgument returns the second positional argument passed to the cli. 46 | func getSecondArgument(cCtx *cli.Context) string { 47 | if cCtx.Args().Len() > 0 { 48 | return cCtx.Args().Get(0) 49 | } 50 | return "" 51 | } 52 | 53 | // getThirdArgument returns the third positional argument passed to the cli. 54 | func getThirdArgument(cCtx *cli.Context) string { 55 | if cCtx.Args().Len() > 0 { 56 | return cCtx.Args().Get(1) 57 | } 58 | return "" 59 | } 60 | 61 | // setEndpoint sets the target Kion installation to interact with. If not 62 | // passed to the tool as an argument, set in the env, or present in the 63 | // configuration dotfile it will prompt the user to provide it. 64 | func (c *Cmd) setEndpoint() error { 65 | if c.config.Kion.URL == "" { 66 | kionURL, err := helper.PromptInput("Kion URL:") 67 | if err != nil { 68 | return err 69 | } 70 | c.config.Kion.URL = kionURL 71 | } 72 | return nil 73 | } 74 | 75 | // getActionAndBuffer determines the action based on the passed flags and sets 76 | // a buffer for the associated action used to determine the cache validity. 77 | func getActionAndBuffer(cCtx *cli.Context) (string, time.Duration) { 78 | // grab the command usage [stak, s, setenv, savecreds, etc] 79 | cmdUsed := cCtx.Lineage()[1].Args().Slice()[0] 80 | 81 | var action string 82 | var buffer time.Duration 83 | if cCtx.Bool("credential-process") { 84 | action = "credential-process" 85 | buffer = 5 86 | } else if cCtx.Bool("print") || cmdUsed == "setenv" { 87 | action = "print" 88 | buffer = 300 89 | } else if cCtx.Bool("save") || cmdUsed == "savecreds" { 90 | action = "save" 91 | buffer = 600 92 | } else { 93 | action = "subshell" 94 | buffer = 300 95 | } 96 | 97 | return action, buffer 98 | } 99 | 100 | // authStakCache handles the common pattern of authenticating the user, 101 | // grabbing a STAK, and caching it. Used to dry up code in various commands. 102 | func (c *Cmd) authStakCache(cCtx *cli.Context, carName string, accNum string, accAlias string) (kion.STAK, error) { 103 | // handle auth 104 | err := c.setAuthToken(cCtx) 105 | if err != nil { 106 | return kion.STAK{}, err 107 | } 108 | 109 | // generate short term tokens 110 | stak, err := kion.GetSTAK(c.config.Kion.URL, c.config.Kion.APIKey, carName, accNum, accAlias) 111 | if err != nil { 112 | return kion.STAK{}, err 113 | } 114 | 115 | // store the stak in the cache 116 | err = c.cache.SetStak(carName, accNum, accAlias, stak) 117 | if err != nil { 118 | return kion.STAK{}, err 119 | } 120 | 121 | return stak, err 122 | } 123 | 124 | // initCache initializes the cache based on the configuration. If the cache 125 | // is disabled or the user has requested to flush the cache, a null cache is 126 | // used. Otherwise, a real cache is initialized using the keyring library. 127 | func (c *Cmd) initCache(cCtx *cli.Context) error { 128 | // if the cache is not disabled, or if the user has requested to flush the 129 | // cache, we initialize the real cache. Otherwise, we use a null cache. 130 | if !c.config.Kion.DisableCache || getThirdArgument(cCtx) == "flush-cache" { 131 | if c.config.Kion.DebugMode { 132 | keyring.Debug = true 133 | } 134 | // initialize the keyring 135 | name := "kion-cli" 136 | ring, err := keyring.Open(keyring.Config{ 137 | ServiceName: name, 138 | KeyCtlScope: "session", 139 | 140 | // osx 141 | KeychainName: "login", 142 | KeychainTrustApplication: true, 143 | KeychainSynchronizable: false, 144 | 145 | // kde wallet 146 | KWalletAppID: name, 147 | KWalletFolder: name, 148 | 149 | // gnome wallet (libsecret) 150 | LibSecretCollectionName: "login", 151 | 152 | // windows 153 | WinCredPrefix: name, 154 | 155 | // password store 156 | PassPrefix: name, 157 | 158 | // encrypted file fallback 159 | FileDir: "~/.kion", 160 | FilePasswordFunc: helper.PromptPassword, 161 | }) 162 | if err != nil { 163 | return err 164 | } 165 | c.cache = cache.NewCache(ring) 166 | } else { 167 | c.cache = cache.NewNullCache() 168 | } 169 | 170 | return nil 171 | } 172 | 173 | // handleProfile checks if a profile is specified and if so, it overrides the 174 | // default configuration with the profile's values. It also honors any global 175 | // flags that were set in the CLI context, allowing them to take precedence over 176 | // the profile's values. 177 | func (c *Cmd) handleProfile(profileName string, cCtx *cli.Context) error { 178 | if profileName != "" { 179 | // grab all manually set global flags so we can honor them over the chosen 180 | // profiles values 181 | // bool values need to be explicitly handled here since we're not iterating 182 | setStrings := make(map[string]string) 183 | 184 | var disableCacheFlagged bool 185 | var debugFlagged bool 186 | var quietFlagged bool 187 | 188 | setGlobalFlags := cCtx.FlagNames() 189 | for _, flag := range setGlobalFlags { 190 | switch flag { 191 | case "endpoint": 192 | setStrings["endpoint"] = c.config.Kion.URL 193 | case "user": 194 | setStrings["user"] = c.config.Kion.Username 195 | case "password": 196 | setStrings["password"] = c.config.Kion.Password 197 | case "idms": 198 | setStrings["idms"] = c.config.Kion.IDMS 199 | case "saml-metadata-file": 200 | setStrings["saml-metadata-file"] = c.config.Kion.SamlMetadataFile 201 | case "saml-sp-issuer": 202 | setStrings["saml-sp-issuer"] = c.config.Kion.SamlIssuer 203 | case "token": 204 | setStrings["token"] = c.config.Kion.APIKey 205 | // non-string flags 206 | case "disable-cache": 207 | disableCacheFlagged = true 208 | case "debug": 209 | debugFlagged = true 210 | case "quiet": 211 | quietFlagged = true 212 | } 213 | } 214 | 215 | // grab the profile and if found and not empty override the default config 216 | profile, found := c.config.Profiles[profileName] 217 | if found { 218 | c.config.Kion = profile.Kion 219 | c.config.Favorites = profile.Favorites 220 | } else { 221 | return fmt.Errorf("profile not found: %s", profileName) 222 | } 223 | 224 | // honor any global flags that were set to maintain precedence 225 | for key, value := range setStrings { 226 | err := cCtx.Set(key, value) 227 | if err != nil { 228 | return err 229 | } 230 | } 231 | 232 | // handle non-string flags 233 | if disableCacheFlagged { 234 | c.config.Kion.DisableCache = true 235 | } 236 | if debugFlagged { 237 | c.config.Kion.DebugMode = true 238 | } 239 | if quietFlagged { 240 | c.config.Kion.QuietMode = true 241 | } 242 | } 243 | return nil 244 | } 245 | 246 | //////////////////////////////////////////////////////////////////////////////// 247 | // // 248 | // Before & After Commands // 249 | // // 250 | //////////////////////////////////////////////////////////////////////////////// 251 | 252 | // BeforeCommands run after the context is ready but before any subcommands are 253 | // executed. Currently used to test feature compatibility with targeted Kion. 254 | func (c *Cmd) BeforeCommands(cCtx *cli.Context) error { 255 | // skip before bits if we don't need them (ie we're just printing help) 256 | args := cCtx.Args().Slice() 257 | if len(args) == 0 || args[0] == "help" || args[0] == "h" { 258 | return nil 259 | } 260 | 261 | // switch profiles if specified 262 | profileName := cCtx.String("profile") 263 | err := c.handleProfile(profileName, cCtx) 264 | if err != nil { 265 | return err 266 | } 267 | 268 | // grab the Kion url if not already set 269 | err = c.setEndpoint() 270 | if err != nil { 271 | return err 272 | } 273 | 274 | // gather the targeted Kion version 275 | kionVer, err := kion.GetVersion(c.config.Kion.URL) 276 | if err != nil { 277 | return err 278 | } 279 | curVer, err := version.NewSemver(kionVer) 280 | if err != nil { 281 | return err 282 | } 283 | 284 | // api/v3/me/cloud-access-role fix constraints 285 | v3mecarC1, _ := version.NewConstraint(">=3.6.29, < 3.7.0") 286 | v3mecarC2, _ := version.NewConstraint(">=3.7.17, < 3.8.0") 287 | v3mecarC3, _ := version.NewConstraint(">=3.8.9, < 3.9.0") 288 | v3mecarC4, _ := version.NewConstraint(">=3.9.0") 289 | 290 | // check constraints and set bool in metadata 291 | if v3mecarC1.Check(curVer) || 292 | v3mecarC2.Check(curVer) || 293 | v3mecarC3.Check(curVer) || 294 | v3mecarC4.Check(curVer) { 295 | cCtx.App.Metadata["useUpdatedCloudAccessRoleAPI"] = true 296 | } 297 | 298 | // SAML metadata file handling 299 | newSaml, _ := version.NewConstraint(">=3.8.0") 300 | if !newSaml.Check(curVer) { 301 | cCtx.App.Metadata["useOldSAML"] = true 302 | } 303 | 304 | // favorites API check 305 | favoritesAPI1, _ := version.NewConstraint(">=3.13.9, < 3.14.0") 306 | favoritesAPI2, _ := version.NewConstraint(">=3.14.3") 307 | if favoritesAPI1.Check(curVer) || favoritesAPI2.Check(curVer) { 308 | cCtx.App.Metadata["useFavoritesAPI"] = true 309 | } 310 | 311 | // initialize the cache 312 | err = c.initCache(cCtx) 313 | if err != nil { 314 | return err 315 | } 316 | 317 | return nil 318 | } 319 | 320 | // AfterCommands run after any subcommands are executed. 321 | func (c *Cmd) AfterCommands(cCtx *cli.Context) error { 322 | return nil 323 | } 324 | -------------------------------------------------------------------------------- /lib/helper/browser.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "os/exec" 10 | "regexp" 11 | "runtime" 12 | "time" 13 | 14 | "github.com/kionsoftware/kion-cli/lib/structs" 15 | ) 16 | 17 | //////////////////////////////////////////////////////////////////////////////// 18 | // // 19 | // Browser // 20 | // // 21 | //////////////////////////////////////////////////////////////////////////////// 22 | 23 | // redirectServer runs a temp go http server to handle logging out any existing 24 | // AWS sessions then redirecting to the federated console login. 25 | func redirectServer(url string, typeID uint) { 26 | // stub out a new mux 27 | mux := http.NewServeMux() 28 | 29 | // handles logout from aws and redirection to login 30 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 31 | redirPage := ` 32 | 33 | 34 | 35 | 36 | Kion-CLI: Redirecting... 37 | 84 | 131 | 132 | 133 |
134 | 135 | 136 | 137 |
138 | 139 | 140 | ` 141 | fmt.Fprintf(w, redirPage, url, typeID) 142 | }) 143 | 144 | // handles callback from client when login is complete 145 | mux.HandleFunc("/done", func(w http.ResponseWriter, r *http.Request) { 146 | fmt.Fprintf(w, "ok") 147 | os.Exit(0) 148 | }) 149 | 150 | // define our server 151 | server := http.Server{ 152 | Addr: ":56092", 153 | Handler: mux, 154 | } 155 | 156 | // start our server 157 | log.Fatal(server.ListenAndServe()) 158 | } 159 | 160 | // OpenBrowser opens up a URL in the users system default browser. It uses a 161 | // local webserver to host a page that handles logging users out of existing 162 | // sessions then redirecting to the federated login page. 163 | // 164 | // Deprecated: Use OpenBrowserRedirect instead. 165 | func OpenBrowser(url string, typeID uint) error { 166 | var err error 167 | 168 | // start our server 169 | go redirectServer(url, typeID) 170 | 171 | // define our open url 172 | serverURL := "http://localhost:56092/" 173 | 174 | switch runtime.GOOS { 175 | case "linux": 176 | err = exec.Command("xdg-open", serverURL).Start() 177 | case "windows": 178 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", serverURL).Start() 179 | case "darwin": 180 | err = exec.Command("open", serverURL).Start() 181 | default: 182 | err = fmt.Errorf("unsupported platform") 183 | } 184 | 185 | // give ourselves up to 5 seconds to complete 186 | time.Sleep(5 * time.Second) 187 | 188 | return err 189 | } 190 | 191 | // OpenBrowserDirect opens up a URL in the users system default browser. It 192 | // uses the redirect_uri query parameter to handle the logout and redirect to 193 | // the federated login page. 194 | func OpenBrowserRedirect(target string, session structs.SessionInfo, config structs.Browser, redirect string, firefoxContainerName string) error { 195 | var err error 196 | var logoutURL string 197 | var replacement string 198 | 199 | // inject a redirect to service if provided 200 | if redirect != "" { 201 | re := regexp.MustCompile(`(Destination=[^&]+)`) 202 | target = re.ReplaceAllStringFunc(target, func(match string) string { 203 | parts := re.FindStringSubmatch(match) 204 | return fmt.Sprintf("%s%s", parts[1], redirect+"%2F") 205 | }) 206 | } 207 | 208 | // determine the logout url based on the account type 209 | switch session.AccountTypeID { 210 | case 1: 211 | // commmercial 212 | logoutURL = "https://signin.aws.amazon.com/oauth?Action=logout&redirect_uri=" 213 | replacement = "://us-east-1.signin" 214 | case 2: 215 | // govcloud 216 | logoutURL = "https://signin.amazonaws-us-gov.com/oauth?Action=logout&redirect_uri=" 217 | replacement = "://us-gov-east-1.signin" 218 | case 4: 219 | // c2s 220 | logoutURL = "http://signin.c2shome.ic.gov/oauth?Action=logout&redirect_uri=" 221 | replacement = "://us-iso-east-1.signin" 222 | case 5: 223 | // sc2s 224 | logoutURL = "http://signin.sc2shome.sgov.gov/oauth?Action=logout&redirect_uri=" 225 | replacement = "://us-isob-east-1.signin" 226 | } 227 | 228 | // update url to one that supports a redirect uri 229 | re := regexp.MustCompile(`:\/\/signin`) 230 | redirectTarget := re.ReplaceAllString(target, replacement) 231 | 232 | // escape the target urls 233 | encodedUrlOriginal := url.QueryEscape(target) 234 | encodedUrlRedirect := url.QueryEscape(redirectTarget) 235 | 236 | // generate the federation link 237 | var federationLink string 238 | if config.FirefoxContainers { 239 | var containerName string 240 | 241 | // use the firefox container name if provided 242 | if firefoxContainerName != "" { 243 | containerName = firefoxContainerName 244 | } else { 245 | containerName = session.AccountName 246 | } 247 | 248 | if runtime.GOOS == "windows" { 249 | federationLink = fmt.Sprintf("ext+container:url=%s^&name=%s", encodedUrlOriginal, url.QueryEscape(containerName)) 250 | } else { 251 | federationLink = fmt.Sprintf("ext+container:url=%s&name=%s", encodedUrlOriginal, url.QueryEscape(containerName)) 252 | } 253 | } else { 254 | federationLink = fmt.Sprintf("%s%s", logoutURL, encodedUrlRedirect) 255 | } 256 | 257 | // open the browser 258 | if config.CustomBrowserPath != "" { 259 | err = exec.Command(config.CustomBrowserPath, federationLink).Start() 260 | } else { 261 | switch runtime.GOOS { 262 | case "linux": 263 | if config.FirefoxContainers { 264 | err = exec.Command("firefox", federationLink).Start() 265 | } else { 266 | err = exec.Command("xdg-open", federationLink).Start() 267 | } 268 | case "windows": 269 | if config.FirefoxContainers { 270 | err = exec.Command("cmd.exe", "/C", "start", "firefox", federationLink).Start() 271 | } else { 272 | err = exec.Command("rundll32", "url.dll,FileProtocolHandler", federationLink).Start() 273 | } 274 | case "darwin": 275 | if config.FirefoxContainers { 276 | err = exec.Command("open", "-a", "firefox", federationLink).Start() 277 | } else { 278 | err = exec.Command("open", federationLink).Start() 279 | } 280 | default: 281 | err = fmt.Errorf("unsupported platform") 282 | } 283 | } 284 | 285 | return err 286 | } 287 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= 2 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= 3 | github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= 4 | github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= 5 | github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= 6 | github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= 7 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 8 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 9 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 10 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 11 | github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= 12 | github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= 13 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 14 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 15 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 16 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 17 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 18 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 19 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 20 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 21 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 22 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 24 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 25 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 26 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= 27 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 28 | github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= 29 | github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 30 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= 34 | github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= 35 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 36 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 37 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= 38 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= 39 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= 40 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= 41 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 42 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 43 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= 44 | github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= 45 | github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= 46 | github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= 47 | github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= 48 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 49 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 50 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 51 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 52 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 53 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 54 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 57 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 58 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 59 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 60 | github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= 61 | github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= 62 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 63 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 64 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 65 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 66 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 67 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 68 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 69 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 70 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 71 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 72 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 73 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 74 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 75 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 76 | github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= 77 | github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 78 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 79 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 80 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 81 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 82 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 85 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 86 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 87 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 88 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 89 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 90 | github.com/russellhaering/gosaml2 v0.9.1 h1:H/whrl8NuSoxyW46Ww5lKPskm+5K+qYLw9afqJ/Zef0= 91 | github.com/russellhaering/gosaml2 v0.9.1/go.mod h1:ja+qgbayxm+0mxBRLMSUuX3COqy+sb0RRhIGun/W2kc= 92 | github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= 93 | github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= 94 | github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= 95 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 96 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 97 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 98 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 99 | github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 100 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 101 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 102 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 103 | github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw= 104 | github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 105 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 106 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 107 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 108 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 109 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 116 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 118 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 119 | golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= 120 | golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= 121 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 122 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 124 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 125 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 126 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 129 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 130 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 131 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 132 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 133 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 134 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 136 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 137 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 138 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/kionsoftware/kion-cli/lib/commands" 11 | "github.com/kionsoftware/kion-cli/lib/helper" 12 | "github.com/kionsoftware/kion-cli/lib/structs" 13 | 14 | "github.com/fatih/color" 15 | "github.com/urfave/cli/v2" 16 | ) 17 | 18 | //////////////////////////////////////////////////////////////////////////////// 19 | // // 20 | // Globals // 21 | // // 22 | //////////////////////////////////////////////////////////////////////////////// 23 | 24 | var ( 25 | config structs.Configuration 26 | configPath string 27 | configFile = ".kion.yml" 28 | 29 | kionCliVersion string 30 | ) 31 | 32 | //////////////////////////////////////////////////////////////////////////////// 33 | // // 34 | // Main // 35 | // // 36 | //////////////////////////////////////////////////////////////////////////////// 37 | 38 | // main defines the command line utilities api. This should probably be broken 39 | // out into its own function some day. 40 | func main() { 41 | // get home directory 42 | home, err := os.UserHomeDir() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | 47 | // allow config file to be overridden by an env var, else use default 48 | userConfigFile := os.Getenv("KION_CONFIG") 49 | if userConfigFile != "" { 50 | configPath = filepath.Clean(userConfigFile) 51 | } else { 52 | configPath = filepath.Join(home, configFile) 53 | } 54 | 55 | // load configuration file 56 | err = helper.LoadConfig(configPath, &config) 57 | if err != nil { 58 | color.Red(" Error: %v", err) 59 | os.Exit(1) 60 | } 61 | 62 | // prep default text for password 63 | passwordDefaultText := "" 64 | if config.Kion.Password != "" { 65 | passwordDefaultText = "*****" 66 | } 67 | 68 | // prep default text for api key 69 | apiKeyDefaultText := "" 70 | if config.Kion.APIKey != "" { 71 | apiKeyDefaultText = "*****" 72 | } 73 | 74 | // convert relative path specified in config file to absolute path 75 | samlMetadataFile := config.Kion.SamlMetadataFile 76 | if samlMetadataFile != "" && !strings.HasPrefix(samlMetadataFile, "http") { 77 | if !filepath.IsAbs(samlMetadataFile) { 78 | // resolve the file path relative to the config path, which is the home directory 79 | samlMetadataFile = filepath.Join(filepath.Dir(configPath), samlMetadataFile) 80 | } 81 | } 82 | 83 | // instantiate commands, populate with config 84 | cmd := commands.NewCommands(&config) 85 | 86 | // define app configuration 87 | app := &cli.App{ 88 | 89 | //////////////// 90 | // Metadata // 91 | //////////////// 92 | 93 | Name: "Kion CLI", 94 | Version: kionCliVersion, 95 | Usage: "Kion federation on the command line!", 96 | EnableBashCompletion: true, 97 | Before: cmd.BeforeCommands, 98 | After: cmd.AfterCommands, 99 | Metadata: map[string]any{ 100 | "useUpdatedCloudAccessRoleAPI": false, 101 | "useOldSAML": false, 102 | "configPath": configPath, 103 | "useFavoritesAPI": false, 104 | }, 105 | 106 | //////////////////// 107 | // Global Flags // 108 | //////////////////// 109 | 110 | Flags: []cli.Flag{ 111 | &cli.StringFlag{ 112 | Name: "endpoint", 113 | Aliases: []string{"url", "e"}, 114 | Value: config.Kion.URL, 115 | EnvVars: []string{"KION_URL"}, 116 | Usage: "Kion `URL`", 117 | Destination: &config.Kion.URL, 118 | }, 119 | &cli.StringFlag{ 120 | Name: "user", 121 | Aliases: []string{"username", "u"}, 122 | Value: config.Kion.Username, 123 | EnvVars: []string{"KION_USERNAME", "CTKEY_USERNAME"}, 124 | Usage: "`USERNAME` for authentication", 125 | Destination: &config.Kion.Username, 126 | }, 127 | &cli.StringFlag{ 128 | Name: "password", 129 | Aliases: []string{"p"}, 130 | Value: config.Kion.Password, 131 | EnvVars: []string{"KION_PASSWORD", "CTKEY_PASSWORD"}, 132 | Usage: "`PASSWORD` for authentication", 133 | Destination: &config.Kion.Password, 134 | DefaultText: passwordDefaultText, 135 | }, 136 | &cli.StringFlag{ 137 | Name: "idms", 138 | Aliases: []string{"i"}, 139 | Value: config.Kion.IDMS, 140 | EnvVars: []string{"KION_IDMS_ID"}, 141 | Usage: "`IDMSID` for authentication", 142 | Destination: &config.Kion.IDMS, 143 | }, 144 | &cli.StringFlag{ 145 | Name: "saml-metadata-file", 146 | Value: samlMetadataFile, 147 | EnvVars: []string{"KION_SAML_METADATA_FILE"}, 148 | Usage: "SAML metadata `FILE` or URL", 149 | Destination: &config.Kion.SamlMetadataFile, 150 | }, 151 | &cli.StringFlag{ 152 | Name: "saml-sp-issuer", 153 | Value: config.Kion.SamlIssuer, 154 | EnvVars: []string{"KION_SAML_SP_ISSUER"}, 155 | Usage: "SAML Service Provider `ISSUER`", 156 | Destination: &config.Kion.SamlIssuer, 157 | }, 158 | &cli.BoolFlag{ 159 | Name: "saml-print-url", 160 | Value: config.Kion.SamlPrintURL, 161 | EnvVars: []string{"KION_SAML_PRINT_URL"}, 162 | Usage: "print SAML URL instead of opening browser", 163 | Destination: &config.Kion.SamlPrintURL, 164 | }, 165 | &cli.StringFlag{ 166 | Name: "token", 167 | Aliases: []string{"t"}, 168 | Value: config.Kion.APIKey, 169 | EnvVars: []string{"KION_API_KEY", "CTKEY_APPAPIKEY"}, 170 | Usage: "`TOKEN` for authentication", 171 | Destination: &config.Kion.APIKey, 172 | DefaultText: apiKeyDefaultText, 173 | }, 174 | &cli.StringFlag{ 175 | Name: "profile", 176 | EnvVars: []string{"KION_PROFILE"}, 177 | Usage: "configuration `PROFILE` to use", 178 | }, 179 | &cli.BoolFlag{ 180 | Name: "disable-cache", 181 | Value: config.Kion.DisableCache, 182 | Usage: "disable the use of caching", 183 | Destination: &config.Kion.DisableCache, 184 | }, 185 | &cli.BoolFlag{ 186 | Name: "debug", 187 | Value: config.Kion.DebugMode, 188 | EnvVars: []string{"KION_DEBUG"}, 189 | Usage: "enable debug mode for verbose logging", 190 | Destination: &config.Kion.DebugMode, 191 | }, 192 | &cli.BoolFlag{ 193 | Name: "quiet", 194 | Value: config.Kion.QuietMode, 195 | EnvVars: []string{"KION_QUIET"}, 196 | Usage: "enable quiet mode to reduce output", 197 | Destination: &config.Kion.QuietMode, 198 | }, 199 | }, 200 | 201 | //////////////// 202 | // Commands // 203 | //////////////// 204 | 205 | Commands: []*cli.Command{ 206 | { 207 | Name: "stak", 208 | Aliases: []string{"setenv", "savecreds", "s"}, 209 | Usage: "Generate short-term access keys", 210 | Before: cmd.ValidateCmdStak, 211 | Action: cmd.GenStaks, 212 | Flags: []cli.Flag{ 213 | &cli.BoolFlag{ 214 | Name: "print", 215 | Aliases: []string{"p"}, 216 | Usage: "print stak only", 217 | }, 218 | &cli.StringFlag{ 219 | Name: "account", 220 | Aliases: []string{"acc", "a"}, 221 | Usage: "target account number, must be passed with car", 222 | }, 223 | &cli.StringFlag{ 224 | Name: "alias", 225 | Aliases: []string{"aka", "l"}, 226 | Usage: "account alias, must be passed with car", 227 | }, 228 | &cli.StringFlag{ 229 | Name: "car", 230 | Aliases: []string{"cloud-access-role", "c"}, 231 | Usage: "target cloud access role, must be passed with account or alias", 232 | }, 233 | &cli.StringFlag{ 234 | Name: "region", 235 | Aliases: []string{"r"}, 236 | Value: config.Kion.DefaultRegion, 237 | EnvVars: []string{"AWS_REGION", "AWS_DEFAULT_REGION"}, 238 | Usage: "target region", 239 | Destination: &config.Kion.DefaultRegion, 240 | }, 241 | &cli.BoolFlag{ 242 | Name: "save", 243 | Aliases: []string{"s"}, 244 | Usage: "save short-term keys as aws credentials profile", 245 | }, 246 | &cli.BoolFlag{ 247 | Name: "credential-process", 248 | Usage: "print stak json as AWS credential process", 249 | }, 250 | }, 251 | }, 252 | { 253 | Name: "console", 254 | Aliases: []string{"con", "c"}, 255 | Usage: "Federate into the web console", 256 | Before: cmd.ValidateCmdConsole, 257 | Action: cmd.FedConsole, 258 | Flags: []cli.Flag{ 259 | &cli.StringFlag{ 260 | Name: "account", 261 | Aliases: []string{"acc", "a"}, 262 | Usage: "target account number, must be passed with car", 263 | }, 264 | &cli.StringFlag{ 265 | Name: "alias", 266 | Aliases: []string{"aka", "l"}, 267 | Usage: "account alias, must be passed with car", 268 | }, 269 | &cli.StringFlag{ 270 | Name: "car", 271 | Aliases: []string{"cloud-access-role", "c"}, 272 | Usage: "target cloud access role, must be passed with account or alias", 273 | }, 274 | }, 275 | }, 276 | { 277 | Name: "favorite", 278 | Aliases: []string{"fav", "f"}, 279 | Usage: "Access favorites via web console or a stak for CLI usage", 280 | ArgsUsage: "[FAVORITE_NAME]", 281 | Action: cmd.Favorites, 282 | Flags: []cli.Flag{ 283 | &cli.BoolFlag{ 284 | Name: "print", 285 | Aliases: []string{"p"}, 286 | Usage: "print stak only", 287 | }, 288 | &cli.StringFlag{ 289 | Name: "access-type", 290 | Aliases: []string{"t"}, 291 | Usage: "account alias, must be passed with car", 292 | }, 293 | &cli.BoolFlag{ 294 | Name: "web", 295 | Aliases: []string{"w"}, 296 | Usage: "shortcut for --access-type=web", 297 | }, 298 | &cli.BoolFlag{ 299 | Name: "credential-process", 300 | Usage: "print stak json as AWS credential process", 301 | }, 302 | }, 303 | BashComplete: func(cCtx *cli.Context) { 304 | // complete if no args are passed 305 | if cCtx.NArg() > 0 { 306 | return 307 | } 308 | // else pass favorites 309 | fNames, _ := helper.MapFavs(config.Favorites) 310 | for _, f := range fNames { 311 | fmt.Println(f) 312 | } 313 | }, 314 | Subcommands: []*cli.Command{ 315 | { 316 | Name: "list", 317 | Usage: "list favorites", 318 | Action: cmd.ListFavorites, 319 | Flags: []cli.Flag{ 320 | &cli.BoolFlag{ 321 | Name: "verbose", 322 | Aliases: []string{"v"}, 323 | Usage: "show full favorite details", 324 | }, 325 | }, 326 | }, 327 | }, 328 | }, 329 | { 330 | Name: "run", 331 | Usage: "Run a command with short-term access keys", 332 | ArgsUsage: "[COMMAND]", 333 | Before: cmd.ValidateCmdRun, 334 | Action: cmd.RunCommand, 335 | Flags: []cli.Flag{ 336 | &cli.StringFlag{ 337 | Name: "favorite", 338 | Aliases: []string{"fav", "f"}, 339 | Usage: "favorite name", 340 | }, 341 | &cli.StringFlag{ 342 | Name: "account", 343 | Aliases: []string{"acc", "a"}, 344 | Usage: "account number", 345 | }, 346 | &cli.StringFlag{ 347 | Name: "alias", 348 | Aliases: []string{"aka", "l"}, 349 | Usage: "account alias", 350 | }, 351 | &cli.StringFlag{ 352 | Name: "car", 353 | Aliases: []string{"c"}, 354 | Usage: "CAR name", 355 | }, 356 | &cli.StringFlag{ 357 | Name: "region", 358 | Aliases: []string{"r"}, 359 | Value: config.Kion.DefaultRegion, 360 | EnvVars: []string{"AWS_REGION", "AWS_DEFAULT_REGION"}, 361 | Usage: "target region", 362 | Destination: &config.Kion.DefaultRegion, 363 | }, 364 | }, 365 | }, 366 | { 367 | Name: "util", 368 | Usage: "Utility commands", 369 | Subcommands: []*cli.Command{ 370 | { 371 | Name: "flush-cache", 372 | Usage: "Flush the Kion CLI cache", 373 | Action: cmd.FlushCache, 374 | }, 375 | { 376 | Name: "push-favorites", 377 | Usage: "Push configured favorites to Kion", 378 | Action: cmd.PushFavorites, 379 | }, 380 | { 381 | Name: "validate-saml", 382 | Usage: "Validate SAML configuration and connectivity", 383 | Action: cmd.ValidateSAML, 384 | }, 385 | }, 386 | }, 387 | }, 388 | } 389 | 390 | // TODO: extend help output to include examples 391 | 392 | // run the app 393 | if err := app.Run(os.Args); err != nil { 394 | color.Red("\nError: %v", err) 395 | os.Exit(1) 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/) with the exception of version 0 as we find our footing. Only changes to the application should be logged here. Repository maintenance, tests, and other non application changes should be excluded. 7 | 8 | [Unreleased] - yyyy-mm-dd 9 | ------------------------- 10 | 11 | Notes for upgrading... 12 | 13 | ### Added 14 | 15 | ### Changed 16 | 17 | ### Deprecated 18 | 19 | ### Removed 20 | 21 | ### Fixed 22 | 23 | [0.14.0] - 2025.09.29 24 | ------------------------- 25 | 26 | Version 0.14.0 integrates favorites between Kion and the Kion CLI (requires Kion version 3.13.5, 3.14.1 or greater). This allows Kion to be the new source of truth for your configured favorites and simplifies maintenance of the Kion CLI configuration file. If you already have favorites defined in your Kion CLI configuration file it is recommended you run `kion util push-favorites` once to sync them with your instance of Kion. 27 | 28 | ### Added 29 | 30 | - Ability to list and use favorites (aliases) that are set upstream in Kion 31 | - New utility command to push local favorites up to Kion (`util push-favorites`) 32 | - Option to delete local favorites once they've been pushed up 33 | - New `default_region` property to the config file. This can be set on any of the Kion profiles and will be used for any favorites for that profile that do not have `region` set. 34 | 35 | [0.13.0] - 2025.07.11 36 | --------------------- 37 | 38 | Version 0.13.0 adds the ability to pass flags to the `favorite` command to override the stored access-type. This enables users to create a generic favorite and then use it to access both cli and the web console. As an example, the following favorite could be created: 39 | 40 | ```yaml 41 | # ~/.kion.yml 42 | favorites: 43 | - name: sandbox 44 | account: "121212121212" 45 | cloud_access_role: Developer 46 | ``` 47 | 48 | Then access it: 49 | 50 | ```bash 51 | # create a cli subshell authenticated to the favorite 52 | kion favorite sandbox 53 | kion fav sandbox 54 | kion f sandbox 55 | 56 | # access the sandbox web console 57 | kion favorite --access-type web sandbox 58 | kion fav -w sandbox 59 | kion f -w sandbox 60 | ``` 61 | 62 | ### Added 63 | 64 | - Allow for favorite access-type overrides [kionsoftware/kion-cli/pull/100] 65 | 66 | [0.12.0] - 2025.06.13 67 | --------------------- 68 | 69 | Version 0.12.0 adds the ability to create custom builds of the Kion CLI with customer specific configurations. This allows for easier team on boarding to the utility. See the __Custom Builds__ section in the repository `README.md` for details on usage. Additionally version 0.12.0 adds new output to the `stak` and `console` commands guiding users on how to create favorites for future use. The new output will be the default behavior but can be disabled with the new `quiet` option, configurable via the config file, `KION_QUIET` environment variable, or global option flag. 70 | 71 | ### Added 72 | 73 | - Ability to create custom Kion CLI binaries with user defined defaults [kionsoftware/kion-cli/pull/96] 74 | - Helper output showing users how to store recently accessed accounts as a favorite [kionsoftware/kion-cli/pull/97] 75 | 76 | [0.11.0] - 2025-06-12 77 | --------------------- 78 | 79 | Version 0.11.0 adds a debug mode (`--debug`) to capture additional output, currently supports keyring operations. Additionally, cache handling is improved when utilizing the `--disable-cache` functionality to ensure the keyring is never initialized. 80 | 81 | ### Added 82 | 83 | - Optional debug mode for increased output. [kionsoftware/kion-cli/pull/95] 84 | 85 | ### Fixed 86 | 87 | - Disabling the cache will now properly prevent keyrings from being accessed. [kionsoftware/kion-cli/pull/94] 88 | 89 | [0.10.0] - 2025-04-29 90 | --------------------- 91 | 92 | Version 0.10.0 adds the ability to target/name Firefox containers when federating with favorites, improves browser support for SAML based authentication, and adds an option for printing the authentication URL as opposed to opening it in the users default browser. The SAML auth URL option can be set with `kion.saml_print_url` in the configuration file, by the `--saml-print-url` flag, or with the `KION_SAML_PRINT_URL` environment variable. Normal precedent order applies. 93 | 94 | ### Added 95 | 96 | - Option to print the SAML authentication url [kionsoftware/kion-cli/pull/84] 97 | - Man page to Homebrew based installs [kionsoftware/kion-cli/pull/79] 98 | - Ability to target a specific Firefox container when federating [kionsoftware/kion-cli/pull/86] 99 | 100 | ### Changed 101 | 102 | - Modified browser calls to be more general and use user defaults [kionsoftware/kion-cli/pull/83] 103 | 104 | [0.9.0] - 2025.01.16 105 | -------------------- 106 | 107 | Version 0.9.0 adds the ability to federate directly into a specific service through favorites or the `console` command. No more hopping into the console dashboard then searching for and navigating to your destination. Note that the specified service is injected into the federation URLs `Destination` parameter. So, for example, the full path for the RDS service is `/rds/home?region=us-east-1#Home`, we just need the first part of the path `rds`. 108 | 109 | ```bash 110 | # Add a service to your favorites 111 | favorites: 112 | - name: mysandbox 113 | account: "121212121212" 114 | cloud_access_role: Admin 115 | access_type: web 116 | service: rds 117 | 118 | # Then call it 119 | kion fav mysandbox 120 | 121 | # Or step through the selection wizard 122 | kion con rds 123 | ``` 124 | 125 | ### Added 126 | 127 | - Ability to federate directly to a service [kionsoftware/kion-cli/pull/77] 128 | 129 | [0.8.0] - 2025.01.07 130 | -------------------- 131 | 132 | Version 0.8.0 adds support for Firefox containers, support for Windows Command Prompt and PowerShell, and improved shell history support when dropping into sub-shells (the `kion stak` command). Firefox container support adds the ability to federate into multiple AWS accounts at the same time for more advanced workflows. Note that Firefox container support requires the [Open external links in a container](https://addons.mozilla.org/en-US/firefox/addon/open-url-in-container) add-on as well as an update to your `~/.kion.yml` configuration file. See the repo `README.md` for more details. A big thank you to @joraff and @mjburling for help with development and testing! 133 | 134 | ### Added 135 | 136 | - Added support for Firefox containers [kionsoftware/kion-cli/pull/72] 137 | - Added support for Windows Command Prompt and PowerShell [kionsoftware/kion-cli/pull/72] 138 | - Configured subshells to pull and set `HISTFILE` in zsh [kionsoftware/kion-cli/pull/70] 139 | 140 | ### Fixed 141 | 142 | - Exit non-zero if an error is encountered [kionsoftware/kion-cli/pull/65] 143 | 144 | [0.7.0] - 2024.09.18 145 | -------------------- 146 | 147 | Version 0.7.0 adds flag support to console federation, addresses a bug that presented when using paths with AWS IAM roles, and adds a method for keeping your Kion password in encrypted storage (eg the system keyring). 148 | 149 | ### Added 150 | 151 | - Added support for flags (account alias/number and car) to console federation [kionsoftware/kion-cli/pull/61] 152 | - Added support for storing login password in encrypted storage [kionsoftware/kion-cli/pull/53] 153 | 154 | ### Fixed 155 | 156 | - Console federation bug when using AWS IAM roles containing a path [kionsoftware/kion-cli/pull/60] 157 | 158 | [0.6.0] - 2024-08-01 159 | -------------------- 160 | 161 | Version 0.6.0 adds support for account aliases coming in Kion 3.9.9 and 3.10.2. Account aliases are globally unique user defined identifiers for accounts stored in Kion. Aliases can be used with the `stak` and `run` commands instead of specifying account numbers. 162 | 163 | ### Added 164 | 165 | - Added support for account aliases [kionsoftware/kion-cli/pull/51] 166 | 167 | 168 | [0.5.0] - 2024-06-24 169 | -------------------- 170 | 171 | This release changes how caching is handled for Gnome users. After upgrading a new empty cache in the default `login` keyring will be used. The old `kion-cli` keyring can be safely removed. 172 | 173 | ### Changed 174 | 175 | - Updated keyring config for Gnome Wallet (libsecret) to use the default `login` keyring [kionsoftware/kion-cli/pull/49] 176 | 177 | 178 | [0.4.1] - 2024-06-24 179 | -------------------- 180 | 181 | ### Fixed 182 | 183 | - Patched the package `github.com/dvsekhvalnov/jose2go` to version 1.6.0 to address Dependabot security findings [kionsoftware/kion-cli/pull/48] 184 | 185 | 186 | [0.4.0] - 2024-06-18 187 | -------------------- 188 | 189 | SAML Authentication is now supported for Kion versions `< 3.8.0`. No additional configuration is required for use, see `README.md` for details on SAML authentication with the CLI. 190 | 191 | ### Added 192 | 193 | - A new version constraint will switch between SAML authentication behaviors based on the target Kion version. [kionsoftware/kion-cli/pull/46] 194 | 195 | [0.3.0] - 2024-06-03 196 | -------------------- 197 | 198 | You can now use Kion CLI with multiple instances of Kion through the use of configuration profiles or by pointing to alternate configuration files. Here are some usage examples: 199 | 200 | ```bash 201 | # point to another configuration file 202 | KION_CONFIG=~/.kion.development.yml kion stak 203 | 204 | # use a 'development' profile within your ~/.kion.yml configuration file 205 | kion --profile development fav sandbox 206 | ``` 207 | 208 | A configuration file for the profile usage example above would look something like this: 209 | 210 | ```yaml 211 | # default profile if none specified 212 | kion: 213 | url: https://kion.mycompany.com 214 | api_key: "app_123" 215 | favorites: 216 | - name: production 217 | account: "232323232323" 218 | cloud_access_role: ReadOnly 219 | 220 | # alternate profiles called with the global `--profile [name]` flag 221 | profiles: 222 | development: 223 | kion: 224 | url: https://dev.kion.mycompany.com 225 | api_key: "app_abc" 226 | favorites: 227 | - name: sandbox 228 | account: "121212121212" 229 | cloud_access_role: Admin 230 | ``` 231 | 232 | ### Added 233 | 234 | - Users can now set a custom config file with the `KION_CONFIG` environment variable [kionsoftware/kion-cli/pull/42] 235 | - Users can define profiles to use Kion CLI with multiple Kion instances [kionsoftware/kion-cli/pull/42] 236 | - Created a `util` command and `flush-cache` subcommand to flush the cache [kionsoftware/kion-cli/pull/42] 237 | 238 | ### Fixed 239 | 240 | - Corrected an issue where the Kion CLI configuration file was not actually optional [kionsoftware/kion-cli/pull/42] 241 | 242 | [0.2.1] - 2024-05-30 243 | -------------------- 244 | 245 | ### Changed 246 | 247 | - Federating into the web console is now handled without iframes or javascript [kionsoftware/kion-cli/pull/40] 248 | 249 | [0.2.0] - 2024-05-24 250 | -------------------- 251 | 252 | Caching and AWS `credential_process` support has been added to the Kion CLI! See the AWS docs [HERE](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html) for more information as well as the `README.md` document in this repo for examples on how to use Kion CLI as a credential provider. 253 | 254 | Kion CLI will now use cached STAKs by default to improve performance and reduce the number of calls to Kion. STAKs will be considered as valid for 15 minutes unless Kion reports back a longer STAK duration. Note that Kion is expected to start returning the duration of a STAK along with the STAK itself starting on versions 3.6.29, 3.7.19, 3.8.13, and 3.9.5. 255 | 256 | The cache will be stored in the system's keychain, and depending on your operating system, you may be prompted to allow Kion CLI to access the cache entry on your first run. 257 | 258 | Cached STAKs will be used by default unless: 259 | - Caching is disabled via the `--disable-cache` global flag 260 | - Caching is disabled in the `~/.kion.yml` configuration file by setting `kion.disable_cache: true` 261 | - The credential has less than 5 seconds left and Kion CLI is being used as an AWS credential provider 262 | - The credential has less than 5 seconds left and Kion CLI is being used to run an ad hoc command 263 | - The credential has less than 5 minutes left and Kion CLI is being used to print keys 264 | - The credential has less than 5 minutes left and Kion CLI is being used to create an authenticated subshell 265 | - The credential has less than 10 minutes left and Kion CLI is being used to create an AWS configuration profile 266 | 267 | Lastly, the following environment variables will no longer be set when using the `run` command to execute ad hoc commands: 268 | 269 | ```bash 270 | KION_ACCOUNT_NUM 271 | KION_ACCOUNT_ALIAS 272 | KION_CAR 273 | ``` 274 | 275 | ### Added 276 | 277 | - Support to use Kion CLI as a credential process subsystem for AWS profiles [kionsoftware/kion-cli/pull/38] 278 | - Add caching for faster operations [kionsoftware/kion-cli/pull/38] 279 | - SAML tokens are now cached for 9.5 minutes [kionsoftware/kion-cli/pull/39] 280 | 281 | ### Changed 282 | 283 | - Kion session data has moved from the `~/.kion.yml` configuration file to the cache [kionsoftware/kion-cli/pull/39] 284 | 285 | ### Removed 286 | 287 | - `KION_*` env variables removed from subshell environments when using the `run` command [kionsoftware/kion-cli/pull/38] 288 | 289 | [0.1.1] - 2024-05-20 290 | -------------------- 291 | 292 | ### Fixed 293 | 294 | - Corrected version number when running `--version` flag [kionsoftware/kion-cli/pull/36] 295 | 296 | [0.1.0] - 2024-05-20 297 | -------------------- 298 | 299 | ### Added 300 | 301 | - Print metadata to stdout when federating into web consoles [kionsoftware/kion-cli/pull/20] 302 | - Add flags to support headless runs [kionsoftware/kion-cli/pull/22] 303 | - Fallback logic for users with restricted perms when using the `run` cmd [kionsoftware/kion-cli/pull/22] 304 | - Logic to accommodate users with cloud access only permissions [kionsoftware/kion-cli/pull/24] 305 | - STAK selection wizard now includes project and car IDs and account numbers [kionsoftware/kion-cli/pull/24] 306 | - Automate AWS logout before federating into the AWS console [kionsoftware/kion-cli/pull/25] 307 | - Support defining region on favorites or via flag [kionsoftware/kion-cli/pull/26] 308 | - Support for old `ctkey` usage by adding compatibility commands [kionsoftware/kion-cli/pull/28] 309 | - Ability to save short-term access keys to an AWS credentials profile [kionsoftware/kion-cli/pull/28] 310 | - Add support for windows when printing STAKs for export [kionsoftware/kion-cli/pull/28] 311 | 312 | ### Changed 313 | 314 | - Renamed `access_type` values for clarity [kionsoftware/kion-cli/pull/11] 315 | - Improve logic around web federation [kionsoftware/kion-cli/pull/21] 316 | - Dynamically output account name info if available [kionsoftware/kion-cli/pull/22] 317 | 318 | ### Fixed 319 | 320 | - Fix unexpected EOF when creating Bash subshells [kionsoftware/kion-cli/pull/14] 321 | - Improve CAR selection logic and usage wording [kionsoftware/kion-cli/pull/19] 322 | - Fix SAML auth around private network access checks [kionsoftware/kion-cli/pull/23] 323 | - Fixed automated logouts of AWS console sessions on Firefox [kionsoftware/kion-cli/pull/31] 324 | 325 | [0.0.2] - 2024-02-23 326 | -------------------- 327 | 328 | ### Added 329 | 330 | - Web console access! [kionsoftware/kion-cli/pull/10] 331 | 332 | ### Fixed 333 | 334 | - Add workaround for users with `Browse Project - Minimal` permissions [kionsoftware/kion-cli/pull/8] 335 | - Ensure STAK output can be eval'd [kionsoftware/kion-cli/pull/1] 336 | 337 | [0.0.1] - 2024-02-02 338 | -------------------- 339 | 340 | Initial release. 341 | --------------------------------------------------------------------------------