granted

package
v0.14.1 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2023 License: MIT Imports: 53 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.LoadProfilesFromDefaultFiles()
		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 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
		}

		grantedFolder, err := config.GrantedConfigFolder()
		if err != nil {
			return err
		}

		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,
					UserDataPath:   path.Join(grantedFolder, "chromium-profiles", "1"),
				}
			case browser.BraveKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
					UserDataPath:   path.Join(grantedFolder, "chromium-profiles", "2"),
				}
			case browser.EdgeKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
					UserDataPath:   path.Join(grantedFolder, "chromium-profiles", "3"),
				}
			case browser.ChromiumKey:
				l = launcher.ChromeProfile{
					ExecutablePath: cfg.CustomBrowserPath,
					UserDataPath:   path.Join(grantedFolder, "chromium-profiles", "4"),
				}
			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
		}
		var needsRefresh bool
		var credentials aws.Credentials
		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)
		if !cfg.DisableCredentialProcessCache {
			creds, ok, err := secureSessionCredentialStorage.GetCredentials(profileName)
			if err != nil {
				return err
			}
			if !ok {
				clio.Debugw("refreshing credentials", "reason", "not found")
				needsRefresh = true
			} else {
				clio.Debugw("credentials found in cache", "expires", creds.Expires.String(), "canExpire", creds.CanExpire, "timeNow", time.Now().String(), "refreshIfBeforeNow", creds.Expires.Add(-c.Duration("window")).String())
				if creds.CanExpire && creds.Expires.Add(-c.Duration("window")).Before(time.Now()) {
					clio.Debugw("refreshing credentials", "reason", "credentials are expired")
					needsRefresh = true
				} else {
					clio.Debugw("using cached credentials")
					credentials = creds
				}
			}
		} else {
			clio.Debugw("refreshing credentials", "reason", "credential process cache is disabled via config")
			needsRefresh = true
		}

		if needsRefresh {
			profiles, err := cfaws.LoadProfilesFromDefaultFiles()
			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
				}
			}
		}

		out := awsCredsStdOut{
			Version:         1,
			AccessKeyID:     credentials.AccessKeyID,
			SecretAccessKey: credentials.SecretAccessKey,
			SessionToken:    credentials.SessionToken,
		}
		if credentials.CanExpire {
			out.Expiration = credentials.Expires.Format(time.RFC3339)
		}

		jsonOut, err := json.Marshal(out)
		if err != nil {
			return errors.Wrap(err, "marshalling session credentials")
		}

		fmt.Println(string(jsonOut))
		return nil
	},
}
View Source
var CredentialsCommand = cli.Command{
	Name:        "credentials",
	Usage:       "Manage secure IAM credentials",
	Subcommands: []*cli.Command{&AddCredentialsCommand, &ImportCredentialsCommand, &UpdateCredentialsCommand, &ListCredentialsCommand, &RemoveCredentialsCommand, &ExportCredentialsCommand, &RotateCredentialsCommand},
}
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 := config.DefaultSharedCredentialsFilename()
			credentialsFile, err := ini.LoadSources(ini.LoadOptions{
				AllowNonUniqueSections:  false,
				SkipUnrecognizableLines: false,
			}, 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 := config.DefaultSharedConfigFilename()
			configFile, err := ini.LoadSources(ini.LoadOptions{
				AllowNonUniqueSections:  false,
				SkipUnrecognizableLines: false,
			}, 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: "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.StringFlag{Name: "region", Usage: "The SSO region. Deprecated, use --sso-region instead. In future, this will be the AWS region for the profile to use", DefaultText: "us-east-1"},
		&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())

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

		if !c.IsSet("region") && !c.IsSet("sso-region") {
			clio.Warnf("Please specify the --sso-region flag: '%s --sso-region us-east-1 %s'", fullCommand, startURL)
			clio.Warn("Currently, Granted defaults to using us-east-1 if this is not provided. In a future version, this flag will be required (https://github.com/common-fate/granted/issues/360)")
		}

		if c.IsSet("region") {
			clio.Warn("Please use --sso-region rather than --region.")
			clio.Warn("In a future version of Granted, the --region flag will be used to specify the 'region' field in generated profiles, rather than the 'sso_region' field. (https://github.com/common-fate/granted/issues/360)")
		}

		region := c.String("sso-region")
		if region == "" {
			region = c.String("region")
		}

		g := awsconfigfile.Generator{
			Config:              ini.Empty(),
			ProfileNameTemplate: c.String("profile-template"),
			NoCredentialProcess: c.Bool("no-credential-process"),
			Prefix:              c.String("prefix"),
		}

		for _, s := range c.StringSlice("source") {
			switch s {
			case "aws-sso":
				g.AddSource(AWSSSOSource{SSORegion: region, StartURL: startURL})
			case "commonfate", "common-fate", "cf":
				ps, err := getCFProfileSource(c, region, 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 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.LoadProfilesFromDefaultFiles()
		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 := config.DefaultSharedCredentialsFilename()
		credentialsFile, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
		}, credentialsFilePath)
		if err != nil {
			return err
		}

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

		configPath := config.DefaultSharedConfigFilename()
		configFile, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
		}, 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 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"},
	},
	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
				}
			}
		}

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

		secureSSOTokenStorage := securestorage.NewSecureSSOTokenStorage()

		newSSOToken, err := cfaws.SSODeviceCodeFlowFromStartUrl(ctx, *cfg, ssoStartUrl)
		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: "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", 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: "region", Usage: "The SSO region. Deprecated, use --sso-region instead. In future, this will be the AWS region for the profile to use", DefaultText: "us-east-1"}, &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())

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

		if !c.IsSet("region") && !c.IsSet("sso-region") {
			clio.Warnf("Please specify the --sso-region flag: '%s --sso-region us-east-1 %s'", fullCommand, startURL)
			clio.Warn("Currently, Granted defaults to using us-east-1 if this is not provided. In a future version, this flag will be required (https://github.com/common-fate/granted/issues/360)")
		}

		if c.IsSet("region") {
			clio.Warn("Please use --sso-region rather than --region.")
			clio.Warn("In a future version of Granted, the --region flag will be used to specify the 'region' field in generated profiles, rather than the 'sso_region' field. (https://github.com/common-fate/granted/issues/360)")
		}

		region := c.String("sso-region")
		if region == "" {
			region = c.String("region")
		}

		configFilename := config.DefaultSharedConfigFilename()

		config, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
		}, 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: c.String("profile-template"),
			NoCredentialProcess: c.Bool("no-credential-process"),
			Prefix:              c.String("prefix"),
			PruneStartURLs:      pruneStartURLs,
		}

		for _, s := range c.StringSlice("source") {
			switch s {
			case "aws-sso":
				g.AddSource(AWSSSOSource{SSORegion: region, StartURL: startURL})
			case "commonfate", "common-fate", "cf":
				ps, err := getCFProfileSource(c, region, 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 := config.DefaultSharedConfigFilename()
		configFile, err := ini.LoadSources(ini.LoadOptions{
			AllowNonUniqueSections:  false,
			SkipUnrecognizableLines: false,
		}, 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"}},
	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
		}

		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"}},
	Action: func(ctx *cli.Context) error {
		url := ctx.String("url")

		secureSSOTokenStorage := securestorage.NewSecureSSOTokenStorage()

		if url != "" {
			token := secureSSOTokenStorage.GetValidSSOToken(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.Context)
		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
		}

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

			var expiry string
			if token == nil {
				expiry = "EXPIRED"
			} else {
				expiry = token.Expiry.Local().Format(time.RFC3339)
			}

			clio.Logf("%-*s (%s) expires at: %s", max, key, strings.Join(startUrlMap[key], ", "), expiry)
		}
		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
}

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
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