granted

package
v0.26.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2024 License: MIT Imports: 62 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var AddCredentialsCommand = cli.Command{
	Name:      "add",
	Usage:     "Add IAM credentials to secure storage",
	ArgsUsage: "[<profile>]",
	Action: func(c *cli.Context) error {
		profileName := c.Args().First()
		if profileName == "" {
			in := survey.Input{Message: "Profile Name:"}
			err := testable.AskOne(&in, &profileName, survey.WithValidator(survey.MinLength(1)))
			if err != nil {
				return err
			}
		}

		profiles, err := cfaws.LoadProfiles()
		if err != nil {
			return err
		}
		if profiles.HasProfile(profileName) {
			return fmt.Errorf("a profile with name %s already exists, you can import an existing profile using '%s credentials import %s", profileName, build.GrantedBinaryName(), profileName)
		}

		credentials, err := promptCredentials()
		if err != nil {
			return err
		}

		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
		err = secureIAMCredentialStorage.StoreCredentials(profileName, credentials)
		if err != nil {
			return err
		}
		err = updateOrCreateProfileWithCredentialProcess(profileName)
		if err != nil {
			return err
		}
		fmt.Printf("Saved %s to secure storage\n", profileName)

		return nil
	},
}
View Source
var CacheCommand = cli.Command{
	Name:        "cache",
	Usage:       "Manage your cached credentials that are stored in secure storage",
	Subcommands: []*cli.Command{&ClearCommand, &ListCommand},
}
View Source
var ClearCommand = cli.Command{
	Name:  "clear",
	Usage: "Clear cached credential from the secure storage",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "storage", Usage: "Specify the storage type"},
		&cli.StringFlag{Name: "profile", Usage: "Specify the profile name of the credential which should be cleared"},
	},
	Action: func(c *cli.Context) error {
		withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)

		selection := c.String("storage")
		if selection == "" {
			in := survey.Select{
				Message: "Select which secure storage would you like to clear cache from",
				Options: []string{"aws-iam-credentials", "sso-token", "session-credentials"},
			}
			clio.NewLine()
			err := testable.AskOne(&in, &selection, withStdio)
			if err != nil {
				return err
			}
		}

		storageToNameMap := map[string]securestorage.SecureStorage{
			"aws-iam-credentials": securestorage.NewSecureIAMCredentialStorage().SecureStorage,
			"sso-token":           securestorage.NewSecureSSOTokenStorage().SecureStorage,
			"session-credentials": securestorage.NewSecureSessionCredentialStorage().SecureStorage,
		}

		selectedStorage := storageToNameMap[selection]
		keys, err := selectedStorage.ListKeys()
		if err != nil {
			return err
		}

		if len(keys) == 0 {
			clio.Warnf("You do not have any cached credentials for %s storage", selection)
			return nil
		}

		selectedProfile := c.String("profile")
		if selectedProfile == "" {
			prompt := &survey.Select{
				Message: "Select the profile name you want to clear cache for",
				Options: keys,
			}
			err = survey.AskOne(prompt, &selectedProfile)
			if err != nil {
				return err
			}
		}

		err = selectedStorage.Clear(selectedProfile)
		if err != nil {
			return err
		}

		clio.Successf("successfully cleared the cached credentials for '%s'", selectedProfile)

		return nil
	},
}
View Source
var ClearSSOTokensCommand = cli.Command{
	Name:  "clear",
	Usage: "Remove a selected token from the keyring",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "all", Aliases: []string{"a"}, Usage: "Remove all saved tokens from keyring"},
	},
	Action: func(c *cli.Context) error {

		if c.Bool("all") {
			err := clearAllTokens()
			if err != nil {
				return err
			}
			clio.Success("Cleared all saved tokens")
			return nil
		}
		var selection string

		if c.Args().First() != "" {
			selection = c.Args().First()
		}

		startUrlMap, err := MapTokens(c.Context)
		if err != nil {
			return err
		}
		if selection == "" {
			var max int
			for k := range startUrlMap {
				if len(k) > max {
					max = len(k)
				}
			}
			selectionsMap := make(map[string]string)
			tokenList := []string{}
			for k, profiles := range startUrlMap {
				stringKey := fmt.Sprintf("%-*s (%s)", max, k, strings.Join(profiles, ", "))
				tokenList = append(tokenList, stringKey)
				selectionsMap[stringKey] = k
			}
			withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
			in := survey.Select{
				Message: "Select a token to remove from keyring",
				Options: tokenList,
			}
			clio.NewLine()
			var out string
			err = testable.AskOne(&in, &out, withStdio)
			if err != nil {
				return err
			}
			selection = selectionsMap[out]
		}

		secureSSOTokenStorage := securestorage.NewSecureSSOTokenStorage()

		err = secureSSOTokenStorage.SecureStorage.Clear(selection)
		if err != nil {
			return err
		}
		clio.Successf("Cleared %s", selection)
		return nil
	},
}
View Source
var CompletionCommand = cli.Command{
	Name:  "completion",
	Usage: "Add autocomplete to your granted cli installation",
	Flags: flags,
	Action: func(c *cli.Context) (err error) {
		shell := c.String("shell")
		switch shell {
		case "fish":
			err = installFishCompletions(c)
		case "zsh":
			err = installZSHCompletions(c)
		case "bash":
			err = installBashCompletions(c)
		default:
			clio.Info("To install completions for other shells, please see our docs: https://docs.commonfate.io/granted/configuration#autocompletion")
		}
		return err
	},

	Description: "Install completions for fish, zsh, or bash. To install completions for other shells, please see our docs:\nhttps://docs.commonfate.io/granted/configuration#autocompletion\n",
}
View Source
var ConsoleCommand = cli.Command{
	Name:  "console",
	Usage: "Generate an AWS console URL using credentials in the environment",
	Flags: []cli.Flag{

		&cli.StringFlag{Name: "service"},
		&cli.StringFlag{Name: "region", EnvVars: []string{"AWS_REGION"}},
		&cli.StringFlag{Name: "destination", Usage: "The destination URL for the console"},
		&cli.BoolFlag{Name: "url", Usage: "Return the URL to stdout instead of launching the browser"},
		&cli.BoolFlag{Name: "firefox", Usage: "Generate the Firefox container URL"},
		&cli.StringFlag{Name: "color", Usage: "When the firefox flag is true, this specifies the color of the container tab"},
		&cli.StringFlag{Name: "icon", Usage: "When firefox flag is true, this specifies the icon of the container tab"},
		&cli.StringFlag{Name: "container-name", Usage: "When firefox flag is true, this specifies the name of the container of the container tab.", Value: "aws"},
	},
	Action: func(c *cli.Context) error {
		ctx := c.Context
		credentials := cfaws.GetEnvCredentials(ctx)
		con := console.AWS{
			Service:     c.String("service"),
			Region:      c.String("region"),
			Destination: c.String("destination"),
		}

		consoleURL, err := con.URL(credentials)
		if err != nil {
			return err
		}

		cfg, err := config.Load()
		if err != nil {
			return err
		}
		if c.Bool("firefox") || cfg.DefaultBrowser == browser.FirefoxKey || cfg.DefaultBrowser == browser.FirefoxStdoutKey {

			consoleURL = fmt.Sprintf("ext+granted-containers:name=%s&url=%s&color=%s&icon=%s", c.String("container-name"), url.QueryEscape(consoleURL), c.String("color"), c.String("icon"))
		}

		justPrintURL := c.Bool("url") || cfg.DefaultBrowser == browser.StdoutKey || cfg.DefaultBrowser == browser.FirefoxStdoutKey
		if justPrintURL {

			fmt.Print(consoleURL)
			return nil
		}

		var l assume.Launcher
		if cfg.CustomBrowserPath == "" && cfg.DefaultBrowser != "" {
			l = launcher.Open{}
		} else if cfg.CustomBrowserPath == "" {
			return errors.New("default browser not configured. run `granted browser set` to configure")
		} else {
			switch cfg.DefaultBrowser {
			case browser.ChromeKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
				}
			case browser.BraveKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
				}
			case browser.EdgeKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
				}
			case browser.ChromiumKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
				}
			case browser.FirefoxKey:
				l = launcher.Firefox{
					ExecutablePath: cfg.CustomBrowserPath,
				}
			case browser.SafariKey:
				l = launcher.Safari{}
			default:
				l = launcher.Open{}
			}
		}

		args := l.LaunchCommand(consoleURL, con.Profile)

		var startErr error
		if l.UseForkProcess() {
			clio.Debugf("running command using forkprocess: %s", args)
			cmd, err := forkprocess.New(args...)
			if err != nil {
				return err
			}
			startErr = cmd.Start()
		} else {
			clio.Debugf("running command without forkprocess: %s", args)
			cmd := exec.Command(args[0], args[1:]...)
			startErr = cmd.Start()
		}

		if startErr != nil {
			return clierr.New(fmt.Sprintf("Granted was unable to open a browser session automatically due to the following error: %s", err.Error()),

				clierr.Info("You can open the browser session manually using the following url:"),
				clierr.Info(consoleURL),
			)
		}
		return nil
	},
}
View Source
var CredentialProcess = cli.Command{
	Name:  "credential-process",
	Usage: "Exports AWS session credentials for use with AWS CLI credential_process",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "profile", Required: true},
		&cli.StringFlag{Name: "url"},
		&cli.DurationFlag{Name: "window", Value: 15 * time.Minute},
		&cli.BoolFlag{Name: "auto-login", Usage: "automatically open the configured browser to log in if needed"},
	},
	Action: func(c *cli.Context) error {
		cfg, err := config.Load()
		if err != nil {
			return err
		}

		profileName := c.String("profile")
		autoLogin := c.Bool("auto-login")
		secureSessionCredentialStorage := securestorage.NewSecureSessionCredentialStorage()
		clio.Debugw("running credential process with config", "profile", profileName, "url", c.String("url"), "window", c.Duration("window"), "disableCredentialProcessCache", cfg.DisableCredentialProcessCache)

		useCache := !cfg.DisableCredentialProcessCache

		if useCache {

			cachedCreds, err := secureSessionCredentialStorage.GetCredentials(profileName)
			if err != nil {
				clio.Debugw("error loading cached credentials", "error", err)
			} else if cachedCreds == nil {
				clio.Debugw("refreshing credentials", "reason", "cachedCreds was nil")
			} else if cachedCreds.CanExpire && cachedCreds.Expires.Add(-c.Duration("window")).Before(time.Now()) {
				clio.Debugw("refreshing credentials", "reason", "credentials are expired")
			} else {

				clio.Debugw("credentials found in cache", "expires", cachedCreds.Expires.String(), "canExpire", cachedCreds.CanExpire, "timeNow", time.Now().String(), "refreshIfBeforeNow", cachedCreds.Expires.Add(-c.Duration("window")).String())
				return printCredentials(*cachedCreds)
			}
		}

		if !useCache {
			clio.Debugw("refreshing credentials", "reason", "credential process cache is disabled via config")
		}

		profiles, err := cfaws.LoadProfiles()
		if err != nil {
			return err
		}

		profile, err := profiles.LoadInitialisedProfile(c.Context, profileName)
		if err != nil {
			return err
		}

		duration := time.Hour
		if profile.AWSConfig.RoleDurationSeconds != nil {
			duration = *profile.AWSConfig.RoleDurationSeconds
		}

		credentials, err := profile.AssumeTerminal(c.Context, cfaws.ConfigOpts{Duration: duration, UsingCredentialProcess: true, CredentialProcessAutoLogin: autoLogin})
		if err != nil {
			return err
		}
		if !cfg.DisableCredentialProcessCache {
			clio.Debugw("storing refreshed credentials in credential process cache", "expires", credentials.Expires.String(), "canExpire", credentials.CanExpire, "timeNow", time.Now().String())
			if err := secureSessionCredentialStorage.StoreCredentials(profileName, credentials); err != nil {
				return err
			}
		}

		return printCredentials(credentials)
	},
}
View Source
var CredentialsCommand = cli.Command{
	Name:        "credentials",
	Usage:       "Manage secure IAM credentials",
	Subcommands: []*cli.Command{&AddCredentialsCommand, &ImportCredentialsCommand, &UpdateCredentialsCommand, &ListCredentialsCommand, &RemoveCredentialsCommand, &ExportCredentialsCommand, &RotateCredentialsCommand, &ImportCredFromEnvCommand},
}
View Source
var DefaultBrowserCommand = cli.Command{
	Name:        "browser",
	Usage:       "View the web browser that Granted uses to open cloud consoles",
	Subcommands: []*cli.Command{&SetBrowserCommand, &SetSSOBrowserCommand},
	Action: func(c *cli.Context) error {

		conf, err := config.Load()
		if err != nil {
			return err
		}
		clio.Infof("Granted is using %s. To change this run `granted browser set`", conf.DefaultBrowser)

		return nil
	},
}
View Source
var ExportCredentialsCommand = cli.Command{
	Name:      "export-plaintext",
	Usage:     "Export credentials from the secure storage to ~/.aws/credentials file in plaintext",
	ArgsUsage: "[<profile>]",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "all", Aliases: []string{"a"}, Usage: "export all credentials from secure storage in plaintext"},
	},
	Action: func(c *cli.Context) error {
		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
		profileName := c.Args().First()
		secureProfileKeys, err := secureIAMCredentialStorage.SecureStorage.ListKeys()
		if err != nil {
			return err
		}
		var profileNames []string
		if c.Bool("all") {
			profileNames = append(profileNames, secureProfileKeys...)
		} else {
			if profileName == "" && len(secureProfileKeys) == 0 {
				fmt.Println("No credentials in secure storage")
				return nil
			}

			if profileName == "" {
				in := survey.Select{Message: "Profile Name:", Options: secureProfileKeys}
				err = testable.AskOne(&in, &profileName)
				if err != nil {
					return err
				}
			}
			profileNames = append(profileNames, profileName)
		}

		for _, profileName := range profileNames {
			credentials, err := secureIAMCredentialStorage.GetCredentials(profileName)
			if err != nil {
				return err
			}

			credentialsFilePath := cfaws.GetAWSCredentialsPath()
			credentialsFile, err := ini.LoadSources(ini.LoadOptions{
				AllowNonUniqueSections:  false,
				SkipUnrecognizableLines: false,
				AllowNestedValues:       true,
			}, credentialsFilePath)
			if err != nil {
				return err
			}

			section, err := credentialsFile.NewSection(profileName)
			if err != nil {
				return err
			}
			err = section.ReflectFrom(&struct {
				AWSAccessKeyID     string `ini:"aws_access_key_id"`
				AWSSecretAccessKey string `ini:"aws_secret_access_key"`
				AWSSessionToken    string `ini:"aws_session_token,omitempty"`
			}{
				AWSAccessKeyID:     credentials.AccessKeyID,
				AWSSecretAccessKey: credentials.SecretAccessKey,
				AWSSessionToken:    credentials.SessionToken,
			})
			if err != nil {
				return err
			}
			err = credentialsFile.SaveTo(credentialsFilePath)
			if err != nil {
				return err
			}
			configPath := cfaws.GetAWSConfigPath()
			configFile, err := ini.LoadSources(ini.LoadOptions{
				AllowNonUniqueSections:  false,
				SkipUnrecognizableLines: false,
				AllowNestedValues:       true,
			}, configPath)
			if err != nil {
				return err
			}
			sectionName := "profile " + profileName
			if section, _ := configFile.GetSection(sectionName); section != nil {
				if section.HasKey("credential_process") {

					if len(section.Keys()) > 1 {
						section.DeleteKey("credential_process")
					} else {
						configFile.DeleteSection(sectionName)
					}
					err = configFile.SaveTo(configPath)
					if err != nil {
						return err
					}

				}
			}

			fmt.Printf("Exported %s in plaintext from secure storage to %s\n", profileName, credentialsFilePath)
			fmt.Printf("The %s credentials have not been removed from secure storage. If you'd like to delete them, you can run '%s credentials remove %s'\n", profileName, build.GrantedBinaryName(), profileName)

		}
		return nil
	},
}
View Source
var GenerateCommand = cli.Command{
	Name:      "generate",
	Usage:     "Prints an AWS configuration file to stdout with profiles from accounts and roles available in AWS SSO",
	UsageText: "granted [global options] sso generate [command options] [sso-start-url]",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "config", Usage: "Specify the SSO config section in the Granted config file ([SSO.name])", Value: "default"},
		&cli.StringFlag{Name: "prefix", Usage: "Specify a prefix for all generated profile names"},
		&cli.StringFlag{Name: "sso-region", Usage: "Specify the SSO region"},
		&cli.StringSliceFlag{Name: "source", Usage: "The sources to load AWS profiles from (valid values are: 'aws-sso', 'commonfate')", Value: cli.NewStringSlice("aws-sso")},
		&cli.BoolFlag{Name: "no-credential-process", Usage: "Generate profiles without the Granted credential-process integration"},
		&cli.StringFlag{Name: "profile-template", Usage: "Specify profile name template", Value: awsconfigfile.DefaultProfileNameTemplate}},
	Action: func(c *cli.Context) error {
		ctx := c.Context
		fullCommand := fmt.Sprintf("%s %s", c.App.Name, c.Command.FullName())

		cfg, err := grantedconfig.Load()
		if err != nil {
			clio.Errorf("Error reading default config (~/.granted/config)")
			return nil
		}

		cfgSSO := cfg.SSO[c.String("config")]
		startURL := coalesceString(c.Args().First(), cfgSSO.StartURL)
		if startURL == "" {
			return clierr.New(fmt.Sprintf("Usage: %s [sso-start-url]", fullCommand), clierr.Infof("For example, %s https://example.awsapps.com/start", fullCommand))
		}

		ssoRegion := coalesceString(c.String("sso-region"), cfgSSO.SSORegion)
		if ssoRegion == "" {
			clio.Errorf("Please specify the --sso-region flag: '%s --sso-region us-east-1 %s'", fullCommand, startURL)
			return nil
		}

		// Since `profile-template` has a default value, need to check IsSet instead of having a value
		var profileNameTemplate string
		if c.IsSet("profile-template") {

			profileNameTemplate = c.String("profile-template")
		} else {

			profileNameTemplate = coalesceString(cfgSSO.ProfileTemplate, c.String("profile-template"))
		}

		prefix := coalesceString(c.String("prefix"), cfgSSO.Prefix)
		noCredentialProcess := c.Bool("no-credential-process") || cfgSSO.NoCredentialProcess

		g := awsconfigfile.Generator{
			Config:              ini.Empty(),
			ProfileNameTemplate: profileNameTemplate,
			NoCredentialProcess: noCredentialProcess,
			Prefix:              prefix,
		}

		for _, s := range c.StringSlice("source") {
			switch s {
			case "aws-sso":
				g.AddSource(AWSSSOSource{SSORegion: ssoRegion, StartURL: startURL})
			case "commonfate", "common-fate", "cf":
				ps, err := getCFProfileSource(c, ssoRegion, startURL)
				if err != nil {
					return err
				}
				g.AddSource(ps)
			default:
				return fmt.Errorf("unknown profile source %s: allowed sources are aws-sso, commonfate", s)
			}
		}

		err = g.Generate(ctx)
		if err != nil {
			return err
		}

		_, err = g.Config.WriteTo(os.Stdout)
		if err != nil {
			return err
		}

		return nil
	},
}

in dev: go run ./cmd/granted/main.go sso generate --sso-region ap-southeast-2 url

View Source
var ImportCredFromEnvCommand = cli.Command{
	Name:  "import-from-env",
	Usage: "Create a new AWS config profile with IAM credentials imported from environment. You must have $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY set in  your environment",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "profile", Required: true},
	},
	Action: func(c *cli.Context) error {
		ctx := c.Context

		accessKeyFromEnv, accessKeyFromEnvExists := os.LookupEnv("AWS_ACCESS_KEY_ID")
		secretAccessKeyFromEnv, secretAccessKeyFromEnvExists := os.LookupEnv("AWS_SECRET_ACCESS_KEY")

		if accessKeyFromEnvExists && secretAccessKeyFromEnvExists {
			profileName := c.String("profile")
			profiles, err := cfaws.LoadProfiles()
			if err != nil {
				return err
			}

			if profiles.HasProfile(profileName) {
				return fmt.Errorf("profile with name '%s' already exist", profileName)
			}

			credentials, err := credentials.NewStaticCredentialsProvider(accessKeyFromEnv, secretAccessKeyFromEnv, "").Retrieve(ctx)
			if err != nil {
				return err
			}

			secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
			err = secureIAMCredentialStorage.StoreCredentials(profileName, credentials)
			if err != nil {
				return err
			}

			credentialsFilePath := cfaws.GetAWSCredentialsPath()
			credentialsFile, err := ini.LoadSources(ini.LoadOptions{
				AllowNonUniqueSections:  false,
				SkipUnrecognizableLines: false,
				AllowNestedValues:       true,
			}, credentialsFilePath)
			if err != nil {
				return err
			}

			section, err := credentialsFile.NewSection(profileName)
			if err != nil {
				return err
			}
			err = section.ReflectFrom(&struct {
				AWSAccessKeyID     string `ini:"aws_access_key_id"`
				AWSSecretAccessKey string `ini:"aws_secret_access_key"`
			}{
				AWSAccessKeyID:     accessKeyFromEnv,
				AWSSecretAccessKey: secretAccessKeyFromEnv,
			})
			if err != nil {
				return err
			}
			err = credentialsFile.SaveTo(credentialsFilePath)
			if err != nil {
				return err
			}

			err = updateOrCreateProfileWithCredentialProcess(profileName)
			if err != nil {
				return err
			}

			clio.Successf("successfully created new profile %s", profileName)

			return nil

		}

		clio.Error("you don't have variables $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY set in your environment.")
		clio.Info("If you instead want to import plain-text credentials from ~/.aws/credentials to secure storage then run 'granted credentials import'")

		return nil

	},
}
View Source
var ImportCredentialsCommand = cli.Command{
	Name:      "import",
	Usage:     "Import plaintext IAM user credentials from AWS credentials file into secure storage",
	ArgsUsage: "[<profile>]",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "overwrite", Usage: "Overwrite an existing profile saved in secure storage with values from the AWS credentials file"},
	},
	Action: func(c *cli.Context) error {
		profileName := c.Args().First()
		profiles, err := cfaws.LoadProfiles()
		if err != nil {
			return err
		}

		if profileName == "" {
			in := survey.Select{Message: "Profile Name:", Options: profiles.ProfileNames}
			err := testable.AskOne(&in, &profileName, survey.WithValidator(func(ans interface{}) error {
				option := ans.(core.OptionAnswer)

				return validateProfileForImport(c.Context, profiles, option.Value, c.Bool("overwrite"))
			}))
			if err != nil {
				return err
			}
		} else {
			err = validateProfileForImport(c.Context, profiles, profileName, c.Bool("overwrite"))
			if err != nil {
				return err
			}
		}

		profile, err := profiles.LoadInitialisedProfile(c.Context, profileName)
		if err != nil {
			return err
		}
		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
		err = secureIAMCredentialStorage.StoreCredentials(profileName, profile.AWSConfig.Credentials)
		if err != nil {
			return err
		}

		err = updateOrCreateProfileWithCredentialProcess(profileName)
		if err != nil {
			return err
		}

		credentialsFilePath := cfaws.GetAWSCredentialsPath()
		credentialsFile, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
			AllowNestedValues:       true,
		}, credentialsFilePath)
		if err != nil {
			return err
		}

		items, err := credentialsFile.GetSection(profileName)
		if err != nil {
			return err
		}

		configPath := cfaws.GetAWSConfigPath()
		configFile, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
			AllowNestedValues:       true,
		}, configPath)
		if err != nil {
			return err
		}
		sectionName := "profile " + profileName

		for _, key := range items.Keys() {

			if !(key.Name() == "aws_access_key_id" || key.Name() == "aws_secret_access_key" || key.Name() == "aws_session_token") {
				section, err := configFile.GetSection(sectionName)
				if err != nil {
					return err
				}
				if !section.HasKey(key.Name()) {
					_, err = section.NewKey(key.Name(), key.Value())
					if err != nil {
						return err
					}
				}
			}
		}

		err = configFile.SaveTo(configPath)
		if err != nil {
			return err
		}

		credentialsFile.DeleteSection(profileName)
		err = credentialsFile.SaveTo(credentialsFilePath)
		if err != nil {
			return err
		}
		fmt.Printf("Saved %s to secure storage\n", profileName)

		return nil
	},
}
View Source
var ListCommand = cli.Command{
	Name:  "list",
	Usage: "List currently cached credentials and secure storage type",
	Action: func(c *cli.Context) error {
		storageToNameMap := map[string]securestorage.SecureStorage{
			"aws-iam-credentials": securestorage.NewSecureIAMCredentialStorage().SecureStorage,
			"sso-token":           securestorage.NewSecureSSOTokenStorage().SecureStorage,
			"session-credentials": securestorage.NewSecureSessionCredentialStorage().SecureStorage,
		}

		tw := tabwriter.NewWriter(os.Stderr, 10, 1, 5, ' ', 0)
		headers := strings.Join([]string{"STORAGE TYPE", "KEY"}, "\t")
		fmt.Fprintln(tw, headers)

		for storageName, v := range storageToNameMap {

			keys, err := v.ListKeys()
			if err != nil {
				return err
			}

			for _, key := range keys {
				tabbed := strings.Join([]string{storageName, key}, "\t")
				fmt.Fprintln(tw, tabbed)
			}

		}

		tw.Flush()

		return nil
	},
}
View Source
var ListCredentialsCommand = cli.Command{
	Name:  "list",
	Usage: "Lists the profile names for credentials in secure storage",
	Action: func(c *cli.Context) error {
		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
		profiles, err := secureIAMCredentialStorage.SecureStorage.List()
		if err != nil {
			return err
		}
		if len(profiles) == 0 {
			clio.Info("No IAM user credentials stored in secure storage")
			return nil
		}
		for _, profile := range profiles {

			fmt.Printf("%s\n", profile.Key)
		}
		return nil
	},
}
View Source
var ListSSOTokensCommand = cli.Command{
	Name:  "list",
	Usage: "Lists all access tokens saved in the keyring",
	Action: func(ctx *cli.Context) error {

		startUrlMap, err := MapTokens(ctx.Context)
		if err != nil {
			return err
		}

		var max int
		for k := range startUrlMap {
			if len(k) > max {
				max = len(k)
			}
		}
		secureSSOTokenStorage := securestorage.NewSecureSSOTokenStorage()
		keys, err := secureSSOTokenStorage.SecureStorage.ListKeys()
		if err != nil {
			return err
		}

		for _, key := range keys {
			clio.Logf("%-*s (%s)", max, key, strings.Join(startUrlMap[key], ", "))
		}
		return nil
	},
}
View Source
var LoginCommand = cli.Command{
	Name:  "login",
	Usage: "Log in via AWS SSO interactive credential process",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "sso-region", Usage: "Specify the SSO region"},
		&cli.StringFlag{Name: "sso-start-url", Usage: "Specify the SSO start url"},
		&cli.StringSliceFlag{Name: "sso-scope", Usage: "Specify the SSO scopes"},
	},
	Action: func(c *cli.Context) error {
		ctx := c.Context
		ssoStartUrl := c.String("sso-start-url")

		if ssoStartUrl == "" {
			in1 := survey.Input{Message: "SSO Start URL"}
			err := testable.AskOne(&in1, &ssoStartUrl)
			if err != nil {
				return err
			}
		}

		ssoRegion := c.String("sso-region")

		if ssoRegion == "" {

			resp, err := http.Get(ssoStartUrl)
			if err != nil {
				return err
			}
			defer resp.Body.Close()

			re := regexp.MustCompile(`<meta\s+name="region"\s+content="(.*?)"/>`)
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return err
			}

			match := re.FindStringSubmatch(string(body))
			if len(match) == 2 {
				ssoRegion = match[1]
			}

			if ssoRegion == "" {
				in2 := survey.Input{Message: "Region"}
				err := testable.AskOne(&in2, &ssoRegion)
				if err != nil {
					return err
				}
			}
		}

		ssoScopes := c.StringSlice("sso-scope")

		cfg := aws.NewConfig()
		cfg.Region = ssoRegion

		secureSSOTokenStorage := securestorage.NewSecureSSOTokenStorage()

		newSSOToken, err := idclogin.Login(ctx, *cfg, ssoStartUrl, ssoScopes)
		if err != nil {
			return err
		}

		secureSSOTokenStorage.StoreSSOToken(ssoStartUrl, *newSSOToken)

		clio.Successf("Successfully logged into Start URL: %s", ssoStartUrl)

		return nil
	},
}
View Source
var PopulateCommand = cli.Command{
	Name:      "populate",
	Usage:     "Populate your local AWS configuration file with profiles from accounts and roles available in AWS SSO",
	UsageText: "granted [global options] sso populate [command options] [sso-start-url]",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "config", Usage: "Specify the SSO config section ([SSO.name])", Value: "default"},
		&cli.StringFlag{Name: "prefix", Usage: "Specify a prefix for all generated profile names"},
		&cli.StringFlag{Name: "sso-region", Usage: "Specify the SSO region"},
		&cli.StringSliceFlag{Name: "sso-scope", Usage: "Specify the SSO scopes"},
		&cli.StringSliceFlag{Name: "source", Usage: "The sources to load AWS profiles from", Value: cli.NewStringSlice("aws-sso")},
		&cli.BoolFlag{Name: "prune", Usage: "Remove any generated profiles with the 'common_fate_generated_from' key which no longer exist"},
		&cli.StringFlag{Name: "profile-template", Usage: "Specify profile name template", Value: awsconfigfile.DefaultProfileNameTemplate},
		&cli.BoolFlag{Name: "no-credential-process", Usage: "Generate profiles without the Granted credential-process integration"},
	},
	Action: func(c *cli.Context) error {
		ctx := c.Context
		fullCommand := fmt.Sprintf("%s %s", c.App.Name, c.Command.FullName())

		cfg, err := grantedconfig.Load()
		if err != nil {
			clio.Errorf("Error reading default config (~/.granted/config)")
			return nil
		}

		cfgSSO := cfg.SSO[c.String("config")]

		startURL := coalesceString(c.Args().First(), cfgSSO.StartURL)
		if startURL == "" {
			return clierr.New(fmt.Sprintf("Usage: %s [sso-start-url]", fullCommand), clierr.Infof("For example, %s https://example.awsapps.com/start", fullCommand))
		}

		ssoRegion := coalesceString(c.String("sso-region"), cfgSSO.SSORegion)
		if ssoRegion == "" {
			clio.Errorf("Please specify the --sso-region flag: '%s --sso-region us-east-1 %s'", fullCommand, startURL)
			return nil
		}

		// Since `profile-template` has a default value, need to check IsSet instead of having a value
		var profileNameTemplate string
		if c.IsSet("profile-template") {

			profileNameTemplate = c.String("profile-template")
		} else {

			profileNameTemplate = coalesceString(cfgSSO.ProfileTemplate, c.String("profile-template"))
		}

		prefix := coalesceString(c.String("prefix"), cfgSSO.Prefix)
		noCredentialProcess := c.Bool("no-credential-process") || cfgSSO.NoCredentialProcess

		configFilename := cfaws.GetAWSConfigPath()

		config, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
			AllowNestedValues:       true,
		}, configFilename)
		if err != nil {
			if !os.IsNotExist(err) {
				return err
			}
			config = ini.Empty()
		}

		var pruneStartURLs []string

		if c.Bool("prune") {
			pruneStartURLs = []string{startURL}
		}

		g := awsconfigfile.Generator{
			Config:              config,
			ProfileNameTemplate: profileNameTemplate,
			NoCredentialProcess: noCredentialProcess,
			Prefix:              prefix,
			PruneStartURLs:      pruneStartURLs,
		}

		for _, s := range c.StringSlice("source") {
			switch s {
			case "aws-sso":
				g.AddSource(AWSSSOSource{SSORegion: ssoRegion, StartURL: startURL, SSOScopes: c.StringSlice("sso-scope")})
			case "commonfate", "common-fate", "cf":
				ps, err := getCFProfileSource(c, ssoRegion, startURL)
				if err != nil {
					return err
				}
				g.AddSource(ps)
			default:
				return fmt.Errorf("unknown profile source %s: allowed sources are aws-sso, commonfate", s)
			}
		}
		err = g.Generate(ctx)
		if err != nil {
			return err
		}

		err = config.SaveTo(configFilename)
		if err != nil {
			return err
		}

		return nil
	},
}
View Source
var RemoveCredentialsCommand = cli.Command{
	Name:      "remove",
	Usage:     "Remove credentials from secure storage and an associated profile if it exists in the AWS config file",
	ArgsUsage: "[<profile>]",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "all", Aliases: []string{"a"}, Usage: "Remove all credentials from secure storage and an associated profile if it exists in the AWS config file"},
	},
	Action: func(c *cli.Context) error {
		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()
		configPath := cfaws.GetAWSConfigPath()
		configFile, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
			AllowNestedValues:       true,
		}, configPath)
		if err != nil {
			return err
		}
		profileName := c.Args().First()
		secureProfileKeys, err := secureIAMCredentialStorage.SecureStorage.ListKeys()
		if err != nil {
			return err
		}
		var profileNames []string
		if c.Bool("all") {
			profileNames = append(profileNames, secureProfileKeys...)
		} else {
			if profileName == "" && len(secureProfileKeys) == 0 {
				fmt.Println("No credentials in secure storage")
				return nil
			}
			if profileName == "" {
				in := survey.Select{Message: "Profile Name:", Options: secureProfileKeys}
				err = testable.AskOne(&in, &profileName)
				if err != nil {
					return err
				}
			}
			profileNames = append(profileNames, profileName)
		}
		fmt.Printf(`Removing credentials from secure storage will cause them to be permanently deleted.
To avoid losing your credentials you may first want to export them to plaintext using 'granted credentials export-plaintext <profile name>'
This command will remove a profile with the same name from the AWS config file if it has a 'credential_process = granted credential-process --profile=<profile name>'
If you have already used 'granted credentials export-plaintext <profile name>' to export the credentials, the profile will not be removed by this command.

`)
		var confirm bool
		s := &survey.Confirm{
			Message: "Are you sure you want to remove these credentials and profile from your AWS config?",
			Default: true,
		}
		err = survey.AskOne(s, &confirm)
		if err != nil {
			return err
		}
		if !confirm {
			fmt.Printf("Cancelled clearing credentials\n")
			return nil
		}

		for _, profileName := range profileNames {
			fmt.Printf("Removing %s credentials from secure storage\n", profileName)
			err = secureIAMCredentialStorage.SecureStorage.Clear(profileName)
			if err != nil {
				return err
			}
			sectionName := "profile " + profileName
			if section, _ := configFile.GetSection(sectionName); section != nil {
				if key, _ := section.GetKey("credential_process"); key != nil {
					if strings.HasPrefix(key.Value(), fmt.Sprintf("%s credential-process", build.GrantedBinaryName())) {
						fmt.Printf("Removing profile %s AWS config file\n", profileName)
						configFile.DeleteSection(sectionName)
					}
				}
			}
		}
		err = configFile.SaveTo(configPath)
		if err != nil {
			return err
		}
		fmt.Printf("Cleared credentials from secure storage\n")
		return nil
	},
}
View Source
var RotateCredentialsCommand = cli.Command{
	Name:  "rotate",
	Usage: "Generates new access key for the profile in AWS, and updates the profile",
	Flags: []cli.Flag{
		&cli.StringFlag{Name: "profile", Usage: "If provided, generates new access key for the specified profile"},
		&cli.BoolFlag{Name: "delete", Usage: "delete the previous active key"},
	},
	Action: func(c *cli.Context) error {
		profileName := c.String("profile")

		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()

		if profileName == "" {
			profileNames, err := secureIAMCredentialStorage.SecureStorage.ListKeys()
			if err != nil {
				return err
			}
			if len(profileNames) == 0 {
				fmt.Println("No credentials in secure storage")
				return nil
			}
			in := survey.Select{Message: "Profile Name:", Options: profileNames}
			err = testable.AskOne(&in, &profileName)
			if err != nil {
				return err
			}
		}

		has, err := secureIAMCredentialStorage.SecureStorage.HasKey(profileName)
		if err != nil {
			return err
		}
		if !has {
			return fmt.Errorf("no credentials exist for %s in secure storage. If you wanted to add a new profile, run '%s credentials add'", profileName, build.GrantedBinaryName())
		}

		var t aws.Credentials
		err = secureIAMCredentialStorage.SecureStorage.Retrieve(profileName, &t)
		if err != nil {
			return err
		}

		opts := []func(*config.LoadOptions) error{

			config.WithSharedConfigProfile(profileName),
		}

		cfg, err := config.LoadDefaultConfig(c.Context, opts...)
		if err != nil {
			return err
		}

		iamClient := iam.NewFromConfig(cfg)

		res, err := iamClient.CreateAccessKey(c.Context, &iam.CreateAccessKeyInput{})
		if err != nil {
			return err
		}

		err = secureIAMCredentialStorage.StoreCredentials(profileName, aws.Credentials{AccessKeyID: *res.AccessKey.AccessKeyId, SecretAccessKey: *res.AccessKey.SecretAccessKey})
		if err != nil {
			return err
		}

		_, err = iamClient.UpdateAccessKey(c.Context, &iam.UpdateAccessKeyInput{AccessKeyId: &t.AccessKeyID, Status: "Inactive"})
		if err != nil {
			return err
		}

		if c.Bool("delete") {
			_, err = iamClient.DeleteAccessKey(c.Context, &iam.DeleteAccessKeyInput{AccessKeyId: &t.AccessKeyID})
			if err != nil {
				return err
			}
		}

		clio.Successf("Access Key of '%s' profile has been successfully rotated and updated in secure storage\n", profileName)

		return nil
	},
}
View Source
var SSOCommand = cli.Command{
	Name:        "sso",
	Usage:       "Manage your local AWS configuration file from information available in AWS SSO",
	Subcommands: []*cli.Command{&GenerateCommand, &PopulateCommand, &LoginCommand},
}
View Source
var SSOTokensCommand = cli.Command{
	Name:        "sso-tokens",
	Usage:       "Manage AWS SSO tokens",
	Subcommands: []*cli.Command{&ListSSOTokensCommand, &ClearSSOTokensCommand, &TokenExpiryCommand},
	Action:      ListSSOTokensCommand.Action,
}
View Source
var SetBrowserCommand = cli.Command{
	Name:  "set",
	Usage: "Change the web browser that Granted uses to open cloud consoles",
	Flags: []cli.Flag{&cli.StringFlag{Name: "browser", Aliases: []string{"b"}, Usage: "Specify a default browser without prompts, e.g `-b firefox`, `-b chrome`"},
		&cli.StringFlag{Name: "path", Aliases: []string{"p"}, Usage: "Specify a path to the browser without prompts, requires -browser to be provided"}},
	Action: func(c *cli.Context) (err error) {
		outcome := c.String("browser")
		path := c.String("path")

		if outcome == "" {
			if path != "" {
				clio.Info("-path flag must be used with -browser flag, provided path will be ignored")
			}
			outcome, err = browser.HandleManualBrowserSelection()
			if err != nil {
				return err
			}
		}

		return browser.ConfigureBrowserSelection(outcome, path)
	},
}
View Source
var SetSSOBrowserCommand = cli.Command{
	Name:  "set-sso",
	Usage: "Change the web browser that Granted uses to sso flows",
	Flags: []cli.Flag{&cli.StringFlag{Name: "browser", Aliases: []string{"b"}, Usage: "Specify a default browser without prompts, e.g `-b firefox`, `-b chrome`"},
		&cli.StringFlag{Name: "path", Aliases: []string{"p"}, Usage: "Specify a path to the browser without prompts, requires -browser to be provided"}},
	Action: func(c *cli.Context) (err error) {
		outcome := c.String("browser")
		path := c.String("path")

		conf, err := config.Load()
		if err != nil {
			return err
		}
		var browserPath string

		if outcome == "" {
			if path != "" {
				clio.Info("-path flag must be used with -browser flag, provided path will be ignored")
			}
			customBrowserPath, err := browser.AskAndGetBrowserPath()
			if err != nil {
				return err
			}
			browserPath = customBrowserPath

		}

		conf.CustomSSOBrowserPath = browserPath
		err = conf.Save()
		if err != nil {
			return err
		}
		clio.Successf("Granted will default to using %s for SSO flows.", browserPath)
		return nil
	},
}
View Source
var TokenCommand = cli.Command{
	Name:  "token",
	Usage: "Deprecated: Use 'sso-tokens' instead",
	Action: func(ctx *cli.Context) error {
		fmt.Println("The 'token' command has been deprecated and will be removed in a future release, it has been renamed to 'sso-tokens'")
		return SSOTokensCommand.Run(ctx)
	},
}

TokenCommand has been deprecated in favour of 'sso-tokens' @TODO: remove this when suitable after deprecation

View Source
var TokenExpiryCommand = cli.Command{
	Name:  "expiry",
	Usage: "Lists expiry status for all access tokens saved in the keyring",
	Flags: []cli.Flag{&cli.StringFlag{Name: "url", Usage: "If provided, prints the expiry of the token for the specific SSO URL"},
		&cli.BoolFlag{Name: "json", Usage: "If provided, prints the expiry of the tokens in JSON"}},
	Action: func(c *cli.Context) error {
		url := c.String("url")
		ctx := c.Context

		secureSSOTokenStorage := securestorage.NewSecureSSOTokenStorage()

		if url != "" {
			token := secureSSOTokenStorage.GetValidSSOToken(ctx, url)

			var expiry string
			if token == nil {
				return errors.New("SSO token is expired")
			}
			expiry = token.Expiry.Local().Format(time.RFC3339)
			fmt.Println(expiry)

			return nil
		}

		startUrlMap, err := MapTokens(ctx)
		if err != nil {
			return err
		}

		var max int
		for k := range startUrlMap {
			if len(k) > max {
				max = len(k)
			}
		}

		keys, err := secureSSOTokenStorage.SecureStorage.ListKeys()
		if err != nil {
			return err
		}

		jsonflag := c.Bool("json")

		type sso_expiry struct {
			StartURLs string `json:"start_urls"`
			ExpiresAt string `json:"expires_at"`
			IsExpired bool   `json:"is_expired"`
		}

		var jsonDataArray []sso_expiry

		for _, key := range keys {
			token := secureSSOTokenStorage.GetValidSSOToken(ctx, key)

			var expiry string
			if token == nil {
				expiry = "EXPIRED"
			} else {
				expiry = token.Expiry.Local().Format(time.RFC3339)
			}
			if jsonflag {
				sso_expiry_data := sso_expiry{
					StartURLs: key,
					ExpiresAt: expiry,
					IsExpired: expiry == "EXPIRED",
				}
				jsonDataArray = append(jsonDataArray, sso_expiry_data)
			} else {
				clio.Logf("%-*s (%s) expires at: %s", max, key, strings.Join(startUrlMap[key], ", "), expiry)
			}
		}

		if jsonflag {
			jsonData, err := json.Marshal(jsonDataArray)
			if err != nil {
				return err
			}
			fmt.Println(string(jsonData))
		}

		return nil
	},
}
View Source
var UninstallCommand = cli.Command{
	Name:  "uninstall",
	Usage: "Remove all Granted configuration",
	Action: func(c *cli.Context) error {
		withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)
		in := &survey.Confirm{
			Message: "Are you sure you want to remove your Granted config?",
			Default: true,
		}
		var confirm bool
		err := survey.AskOne(in, &confirm, withStdio)
		if err != nil {
			return err
		}
		if confirm {

			err = alias.UninstallDefaultShellAlias()
			if err != nil {
				clio.Error(err.Error())
			}
			grantedFolder, err := config.GrantedConfigFolder()
			if err != nil {
				return err
			}
			err = os.RemoveAll(grantedFolder)
			if err != nil {
				return err
			}

			clio.Successf("Removed Granted config folder %s\n", grantedFolder)
		}
		return nil
	},
}
View Source
var UpdateCredentialsCommand = cli.Command{
	Name:      "update",
	Usage:     "Update existing credentials in secure storage",
	ArgsUsage: "[<profile>]",
	Action: func(c *cli.Context) error {
		profileName := c.Args().First()
		secureIAMCredentialStorage := securestorage.NewSecureIAMCredentialStorage()

		if profileName == "" {
			profileNames, err := secureIAMCredentialStorage.SecureStorage.ListKeys()
			if err != nil {
				return err
			}
			if profileName == "" && len(profileNames) == 0 {
				fmt.Println("No credentials in secure storage")
				return nil
			}
			in := survey.Select{Message: "Profile Name:", Options: profileNames}
			err = testable.AskOne(&in, &profileName)
			if err != nil {
				return err
			}
		}

		has, err := secureIAMCredentialStorage.SecureStorage.HasKey(profileName)
		if err != nil {
			return err
		}
		if !has {
			return fmt.Errorf("no credentials exist for %s in secure storage. If you wanted to add a new profile, run '%s credentials add'", profileName, build.GrantedBinaryName())
		}

		credentials, err := promptCredentials()
		if err != nil {
			return err
		}
		err = secureIAMCredentialStorage.StoreCredentials(profileName, credentials)
		if err != nil {
			return err
		}

		fmt.Printf("Updated %s in secure storage\n", profileName)

		return nil
	},
}

Functions

func GetCliApp

func GetCliApp() *cli.App

func MapTokens added in v0.1.8

func MapTokens(ctx context.Context) (map[string][]string, error)

Types

type AWSSSOSource added in v0.8.0

type AWSSSOSource struct {
	SSORegion string
	StartURL  string
	SSOScopes []string
}

func (AWSSSOSource) GetProfiles added in v0.8.0

func (s AWSSSOSource) GetProfiles(ctx context.Context) ([]awsconfigfile.SSOProfile, error)

type AutoCompleteTemplateData added in v0.2.1

type AutoCompleteTemplateData struct {
	Program string
}

Directories

Path Synopsis
Package awsmerge contains logic to merge multiple AWS config files together.
Package awsmerge contains logic to merge multiple AWS config files together.
exp
Package exp holds experimental commands.
Package exp holds experimental commands.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL