From 209270d35fc87ba86fa6adee7478665f7521820e Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:54:03 -0600 Subject: [PATCH 01/34] Add AWS credentials command to retrieve temporary credentials using AWS SSO This commit introduces a new command `aws-credentials` that allows users to obtain AWS temporary credentials for a specified profile. The command verifies user access, reads the AWS configuration, and retrieves credentials via the AWS SSO API. It includes error handling for various scenarios, such as missing configurations and access denials, and outputs the credentials in JSON format. --- cmd/cone/aws_credentials.go | 360 ++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 cmd/cone/aws_credentials.go diff --git a/cmd/cone/aws_credentials.go b/cmd/cone/aws_credentials.go new file mode 100644 index 00000000..4c628788 --- /dev/null +++ b/cmd/cone/aws_credentials.go @@ -0,0 +1,360 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" + "github.com/conductorone/cone/pkg/client" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// AWSCredentials represents the structure of AWS temporary credentials +// that will be output in JSON format +type AWSCredentials struct { + Version int `json:"Version"` + AccessKeyId string `json:"AccessKeyId"` + SecretAccessKey string `json:"SecretAccessKey"` + SessionToken string `json:"SessionToken"` + Expiration string `json:"Expiration"` +} + +// RoleCredentialsResponse represents the response from AWS SSO get-role-credentials API +type RoleCredentialsResponse struct { + RoleCredentials struct { + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + SessionToken string `json:"sessionToken"` + Expiration int64 `json:"expiration"` + } `json:"roleCredentials"` +} + +// awsCredentialsCmd creates the cobra command for getting AWS credentials +// Usage: cone aws-credentials +func awsCredentialsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "aws-credentials ", + Short: "Get AWS credentials for a profile", + RunE: awsCredentialsRun, + } + return cmd +} + +// awsCredentialsRun is the main function that handles getting AWS credentials +// It verifies access, reads AWS config, and retrieves temporary credentials +func awsCredentialsRun(cmd *cobra.Command, args []string) error { + ctx, _, _, err := cmdContext(cmd) + if err != nil { + return err + } + + if err := validateArgLenth(1, args, cmd); err != nil { + return err + } + + profileName := args[0] + + // Check if user has access to this permission set + hasAccess, err := checkC1Access(ctx, profileName) + if err != nil { + return fmt.Errorf("failed to check C1 access: %w", err) + } + + if !hasAccess { + fmt.Fprintf(os.Stderr, "You do not have access to this permission set.\n") + fmt.Fprintf(os.Stderr, "To request access, run: cone get %s --wait\n", profileName) + fmt.Fprintf(os.Stderr, "This will allow you to specify justification and duration for your access request.\n") + return fmt.Errorf("access denied: please request access using 'cone get %s --wait'", profileName) + } + + awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws") + configPath := filepath.Join(awsConfigDir, "config") + + configContent, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read AWS config: %w", err) + } + + configStr := string(configContent) + profileSection := fmt.Sprintf("[profile %s]", profileName) + profileConfig := extractProfileConfig(configStr, profileSection) + + accountID := extractConeSSOAccountID(profileConfig) + roleName := extractConeSSORoleName(profileConfig) + ssoStartURL := extractConeSSOStartURL(profileConfig) + ssoRegion := extractConeSSORegion(profileConfig) + + if accountID == "" || roleName == "" || ssoStartURL == "" { + return fmt.Errorf("missing required SSO configuration for profile %s", profileName) + } + + if err := verifySSOSession(ssoStartURL, ssoRegion); err != nil { + return fmt.Errorf("SSO session verification failed: %w", err) + } + + creds, err := getTemporaryCredentials(accountID, roleName) + if err != nil { + return fmt.Errorf("failed to get temporary credentials: %w", err) + } + + output := AWSCredentials{ + Version: 1, + AccessKeyId: creds.AccessKeyId, + SecretAccessKey: creds.SecretAccessKey, + SessionToken: creds.SessionToken, + Expiration: creds.Expiration, + } + + jsonOutput, err := json.Marshal(output) + if err != nil { + return fmt.Errorf("failed to marshal credentials: %w", err) + } + + fmt.Println(string(jsonOutput)) + return nil +} + +// extractProfileConfig extracts the configuration section for a specific AWS profile +// from the AWS config file +func extractProfileConfig(config, profileSection string) string { + lines := strings.Split(config, "\n") + var profileLines []string + inProfile := false + + for _, line := range lines { + if line == profileSection { + inProfile = true + continue + } + if inProfile { + if strings.HasPrefix(line, "[") { + break + } + profileLines = append(profileLines, line) + } + } + + return strings.Join(profileLines, "\n") +} + +// extractConeSSOAccountID extracts the AWS account ID from the profile configuration +// This is used to identify which AWS account to get credentials for +func extractConeSSOAccountID(profileConfig string) string { + for _, line := range strings.Split(profileConfig, "\n") { + if strings.HasPrefix(line, "cone_sso_account_id") { + parts := strings.Split(line, "=") + if len(parts) == 2 { + return strings.TrimSpace(parts[1]) + } + } + } + return "" +} + +// extractConeSSORoleName extracts the AWS role name from the profile configuration +// This is the role that will be assumed when getting credentials +func extractConeSSORoleName(profileConfig string) string { + for _, line := range strings.Split(profileConfig, "\n") { + if strings.HasPrefix(line, "cone_sso_role_name") { + parts := strings.Split(line, "=") + if len(parts) == 2 { + return strings.TrimSpace(parts[1]) + } + } + } + return "" +} + +// extractConeSSOStartURL extracts the AWS SSO start URL from the profile configuration +// This is the URL used to initiate the SSO login process +func extractConeSSOStartURL(profileConfig string) string { + for _, line := range strings.Split(profileConfig, "\n") { + if strings.HasPrefix(line, "cone_sso_start_url") { + parts := strings.Split(line, "=") + if len(parts) == 2 { + return strings.TrimSpace(parts[1]) + } + } + } + return "" +} + +// extractConeSSORegion extracts the AWS region from the profile configuration +// Defaults to us-east-1 if not specified +func extractConeSSORegion(profileConfig string) string { + for _, line := range strings.Split(profileConfig, "\n") { + if strings.HasPrefix(line, "cone_sso_region") { + parts := strings.Split(line, "=") + if len(parts) == 2 { + return strings.TrimSpace(parts[1]) + } + } + } + return "us-east-1" // Default region +} + +// getSSOToken retrieves a valid SSO token from the AWS SSO cache +// It looks for a token that matches the given start URL and hasn't expired +func getSSOToken(ssoStartURL string) (string, error) { + cacheDir := filepath.Join(os.Getenv("HOME"), ".aws", "sso", "cache") + files, err := os.ReadDir(cacheDir) + if err != nil { + return "", fmt.Errorf("failed to read SSO cache directory: %w", err) + } + + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".json") { + content, err := os.ReadFile(filepath.Join(cacheDir, file.Name())) + if err != nil { + continue + } + + var cache struct { + AccessToken string `json:"accessToken"` + ExpiresAt time.Time `json:"expiresAt"` + StartURL string `json:"startUrl"` + } + if err := json.Unmarshal(content, &cache); err != nil { + continue + } + + if cache.StartURL == ssoStartURL && cache.ExpiresAt.After(time.Now()) { + return cache.AccessToken, nil + } + } + } + + return "", fmt.Errorf("no valid SSO token found for %s", ssoStartURL) +} + +// getTemporaryCredentials retrieves temporary AWS credentials using AWS SSO +// It handles the SSO login process if needed and returns the credentials +func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error) { + ssoStartURL := viper.GetString("aws_sso_start_url") + if ssoStartURL == "" { + return nil, fmt.Errorf("missing AWS SSO URL. Please run 'cone config-aws set-sso-url ' first") + } + + token, err := getSSOToken(ssoStartURL) + if err != nil { + loginCmd := exec.Command("aws", "sso", "login", "--sso-session", "cone-sso") + loginCmd.Stdout = nil + loginCmd.Stderr = nil + _ = loginCmd.Run() // ignore output, just try to login + token, err = getSSOToken(ssoStartURL) + if err != nil { + return nil, fmt.Errorf("failed to get token after login: %w", err) + } + } + + cmd := exec.Command("aws", "sso", "get-role-credentials", + "--access-token", token, + "--account-id", accountID, + "--role-name", roleName, + "--region", "us-east-1", + "--output", "json") + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + if err != nil { + if strings.Contains(stderr.String(), "AccessDenied") { + return nil, fmt.Errorf("access denied: you don't have access to this role") + } + return nil, fmt.Errorf("failed to get credentials: %w\nCommand output: %s\nError output: %s", + err, stdout.String(), stderr.String()) + } + + var response RoleCredentialsResponse + if err := json.Unmarshal(stdout.Bytes(), &response); err != nil { + return nil, fmt.Errorf("failed to parse credentials: %w\nCommand output: %s", + err, stdout.String()) + } + + creds := &AWSCredentials{ + Version: 1, + AccessKeyId: response.RoleCredentials.AccessKeyId, + SecretAccessKey: response.RoleCredentials.SecretAccessKey, + SessionToken: response.RoleCredentials.SessionToken, + Expiration: time.UnixMilli(response.RoleCredentials.Expiration).Format(time.RFC3339), + } + + return creds, nil +} + +// checkC1Access verifies if the user has access to the requested AWS profile +// by checking their grants in ConductorOne +func checkC1Access(ctx context.Context, profileName string) (bool, error) { + cmd := &cobra.Command{} + cmd.SetContext(ctx) + _, c1Client, _, err := cmdContext(cmd) + if err != nil { + return false, fmt.Errorf("failed to get C1 client: %w", err) + } + + // Get current user ID + userIntro, err := c1Client.AuthIntrospect(ctx) + if err != nil { + return false, fmt.Errorf("failed to get user info: %w", err) + } + userID := client.StringFromPtr(userIntro.UserID) + + // Search for the entitlement by alias (profile name) + entitlements, err := c1Client.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ + EntitlementAlias: profileName, + GrantedStatus: shared.GrantedStatusAll, + }) + if err != nil { + return false, fmt.Errorf("failed to search entitlements: %w", err) + } + + if len(entitlements) == 0 { + return false, fmt.Errorf("no entitlements found matching profile name: %s", profileName) + } + + // Check grants for each matching entitlement + for _, entitlement := range entitlements { + grants, err := c1Client.GetGrantsForIdentity(ctx, client.StringFromPtr(entitlement.Entitlement.AppID), client.StringFromPtr(entitlement.Entitlement.ID), userID) + if err != nil { + return false, fmt.Errorf("failed to check grants: %w", err) + } + + // Check if user has an active grant + for _, grant := range grants { + if grant.CreatedAt != nil && grant.DeletedAt == nil { + return true, nil + } + } + } + + return false, nil +} + +// verifySSOSession checks if the AWS SSO session is properly configured +// in the AWS config file +func verifySSOSession(ssoStartURL, ssoRegion string) error { + awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws") + configPath := filepath.Join(awsConfigDir, "config") + configContent, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read AWS config: %w", err) + } + + configStr := string(configContent) + sessionSection := "[sso-session cone-sso]" + if !strings.Contains(configStr, sessionSection) { + return fmt.Errorf("SSO session configuration not found in AWS config") + } + + return nil +} From 4584254a2b2b1b03b8cfd65f124be721acbaa434 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:55:07 -0600 Subject: [PATCH 02/34] Add root command for Cone CLI with multiple subcommands This commit introduces the root command for the Cone CLI, which interacts with the ConductorOne API to manage access to entitlements. It includes persistent flags for configuration options such as profile, client ID, and output format, as well as various subcommands for managing tasks, user information, and AWS credentials. --- cmd/cone/cmd.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/cmd/cone/cmd.go b/cmd/cone/cmd.go index 0f363248..568f1e0e 100644 --- a/cmd/cone/cmd.go +++ b/cmd/cone/cmd.go @@ -44,3 +44,42 @@ func validateArgLenth(expectedCount int, args []string, cmd *cobra.Command) erro return fmt.Errorf("expected %d arguments, got %d\n%s", expectedCount, len(args), cmd.UsageString()) } + +func rootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "cone", + Short: "Cone interacts with the ConductorOne API to manage access to entitlements.", + SilenceUsage: true, + SilenceErrors: true, + } + + cmd.PersistentFlags().StringP("profile", "p", "default", "The config profile to use.") + cmd.PersistentFlags().BoolP("non-interactive", "i", false, "Disable prompts.") + cmd.PersistentFlags().String("client-id", "", "Client ID") + cmd.PersistentFlags().String("client-secret", "", "Client secret") + cmd.PersistentFlags().String("api-endpoint", "", "Override the API endpoint") + cmd.PersistentFlags().StringP("output", "o", "table", "Output format. Valid values: table, json, json-pretty, wide.") + cmd.PersistentFlags().Bool("debug", false, "Enable debug logging") + + cmd.AddCommand(getCmd()) + cmd.AddCommand(dropCmd()) + cmd.AddCommand(approveTasksCmd()) + cmd.AddCommand(denyTasksCmd()) + cmd.AddCommand(getTasksCmd()) + cmd.AddCommand(tasksCommentCmd()) + cmd.AddCommand(escalateTasksCmd()) + cmd.AddCommand(configAwsCmd()) + cmd.AddCommand(whoAmICmd()) + cmd.AddCommand(getUserCmd()) + cmd.AddCommand(searchEntitlementsCmd()) + cmd.AddCommand(tasksCmd()) + cmd.AddCommand(loginCmd()) + cmd.AddCommand(hasCmd()) + cmd.AddCommand(tokenCmd()) + cmd.AddCommand(terraformCmd()) + cmd.AddCommand(decryptCredentialCmd()) + cmd.AddCommand(awsCredentialsCmd()) + cmd.AddCommand(generateAliasCmd()) + + return cmd +} From 87678545eefababf1488c9ead4b339da4c6fd959 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:55:22 -0600 Subject: [PATCH 03/34] Implement AWS SSO configuration commands in Cone CLI This commit introduces the `config-aws` command for managing AWS SSO settings, including subcommands to set and get the AWS SSO start URL. The command names have been updated for clarity and consistency, enhancing the user experience for AWS credential management and permission set operations. --- cmd/cone/config.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/cmd/cone/config.go b/cmd/cone/config.go index 453f6549..c3fd1644 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -97,3 +97,80 @@ func getCredentials(v *viper.Viper) (string, string, error) { } return clientId, clientSecret, nil } + +// configAwsCmd creates the main AWS configuration command +// This command was renamed from 'config' to 'config-aws' to be more specific about its AWS functionality +// It provides subcommands for managing AWS SSO settings, particularly the SSO start URL +// which is required for AWS credential management and permission set operations +func configAwsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "config-aws", + Short: "Manage AWS SSO configuration for ConductorOne", + Long: `Manage AWS SSO configuration for ConductorOne. +This command helps you configure AWS SSO settings needed for AWS credential management. +It allows you to set and get the AWS SSO start URL, which is required for: +- Getting AWS credentials via 'cone aws-credentials' +- Managing AWS permission sets +- Accessing AWS resources through ConductorOne`, + } + + cmd.AddCommand(setAWSSSOStartURLCmd()) + cmd.AddCommand(getAWSSSOStartURLCmd()) + + return cmd +} + +// setAWSSSOStartURLCmd creates the command for setting the AWS SSO start URL +// This command was renamed from 'set-aws-sso-url' to 'set-sso-url' for simplicity +// The URL is stored in the Viper configuration and is used by other AWS-related commands +// to authenticate with AWS SSO and manage permissions +func setAWSSSOStartURLCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "set-sso-url ", + Short: "Set the AWS SSO start URL for your organization", + Long: `Set the AWS SSO start URL for your organization. +This URL is required for AWS SSO authentication and is used when: +- Getting AWS credentials via 'cone aws-credentials' +- Managing AWS permission sets +- Accessing AWS resources through ConductorOne + +Example: cone config-aws set-sso-url https://your-org.awsapps.com/start`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + url := args[0] + viper.Set("aws_sso_start_url", url) + if err := viper.WriteConfig(); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + fmt.Printf("AWS SSO start URL set to: %s\n", url) + return nil + }, + } + return cmd +} + +// getAWSSSOStartURLCmd creates the command for retrieving the current AWS SSO start URL +// This command was renamed from 'get-aws-sso-url' to 'get-sso-url' for consistency +// It reads the URL from the Viper configuration and displays it to the user +// If no URL is set, it informs the user that the URL needs to be configured +func getAWSSSOStartURLCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get-sso-url", + Short: "Get the currently configured AWS SSO start URL", + Long: `Get the currently configured AWS SSO start URL. +This URL is used for AWS SSO authentication and is required for: +- Getting AWS credentials via 'cone aws-credentials' +- Managing AWS permission sets +- Accessing AWS resources through ConductorOne`, + RunE: func(cmd *cobra.Command, args []string) error { + url := viper.GetString("aws_sso_start_url") + if url == "" { + fmt.Println("AWS SSO start URL is not set") + return nil + } + fmt.Printf("AWS SSO start URL: %s\n", url) + return nil + }, + } + return cmd +} From d47a1daaf62d988e563b4ecc82094fcd1ce2e308 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:55:37 -0600 Subject: [PATCH 04/34] Add alias generation command to Cone CLI This commit introduces the `generate-alias` command for the Cone CLI, enabling users to generate aliases for entitlements in ConductorOne. The command supports various alias schemas and includes options for filtering entitlements, customizing formats, and previewing changes. It also provides progress updates and a summary of the alias generation process, enhancing the management of entitlement aliases. --- cmd/cone/generate_alias.go | 512 +++++++++++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 cmd/cone/generate_alias.go diff --git a/cmd/cone/generate_alias.go b/cmd/cone/generate_alias.go new file mode 100644 index 00000000..a81f27b6 --- /dev/null +++ b/cmd/cone/generate_alias.go @@ -0,0 +1,512 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "os" + "regexp" + "strings" + + "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" + "github.com/conductorone/cone/pkg/client" + "github.com/pterm/pterm" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Stats tracks the progress of alias generation +type Stats struct { + Total int + Processed int + Skipped int + Updated int + Failed int + Errors []string +} + +// Generic entitlement words that should be replaced with resource names +var genericEntitlementWords = []string{ + "member", + "assignment", + "access", + "role", + "group", +} + +// Words to remove from display names +var wordsToRemove = []string{ + "Role member", "role member", "RoleMember", "roleMember", + "Group member", "group member", "GroupMember", "groupMember", + "Member", "member", + "Role", "role", + "Group", "group", +} + +// cleanText processes a string to create a valid alias by: +// 1. Removing text in parentheses +// 2. Removing unnecessary words +// 3. Converting to lowercase +// 4. Replacing spaces with hyphens +// 5. Removing invalid characters +// 6. Ensuring proper length and format +func cleanText(text string) string { + // Remove anything in parentheses + text = regexp.MustCompile(`\s*\([^)]*\)`).ReplaceAllString(text, "") + + // Remove unnecessary words + for _, word := range wordsToRemove { + text = strings.ReplaceAll(text, word, "") + } + + // Clean up text + text = strings.TrimSpace(text) + text = regexp.MustCompile(`\s+`).ReplaceAllString(text, " ") + + // Convert to lowercase and replace spaces with hyphens + text = strings.ToLower(text) + text = strings.ReplaceAll(text, " ", "-") + + // Remove any characters that aren't allowed + text = regexp.MustCompile(`[^a-z0-9\-_\.]`).ReplaceAllString(text, "") + + // Remove any double hyphens + text = strings.ReplaceAll(text, "--", "-") + + // Ensure it starts and ends with a letter or digit + text = strings.Trim(text, "-_.") + if text == "" { + text = "entitlement" // fallback if we end up with empty string + } + + // Ensure it's not too long + if len(text) > 63 { + text = text[:63] + // Make sure we don't end with a hyphen + text = strings.TrimRight(text, "-") + } + + return text +} + +// isGenericEntitlement checks if an entitlement name is too generic to be useful +func isGenericEntitlement(name string) bool { + name = strings.ToLower(strings.TrimSpace(name)) + for _, word := range genericEntitlementWords { + if name == word { + return true + } + } + return false +} + +// getResourceName extracts and cleans the resource name from an entitlement +func getResourceName(e *client.EntitlementWithBindings) string { + if appResource := client.GetExpanded[shared.AppResource](e, client.ExpandedAppResource); appResource != nil && appResource.DisplayName != nil { + return cleanText(*appResource.DisplayName) + } + // Fallback to app name if no resource name is available + if app := client.GetExpanded[shared.App](e, client.ExpandedApp); app != nil && app.DisplayName != nil { + return cleanText(*app.DisplayName) + } + return "resource" // Final fallback +} + +// generateAliasRun is the main function that handles alias generation +func generateAliasRun(cmd *cobra.Command, args []string) error { + ctx, c, v, err := cmdContext(cmd) + if err != nil { + return err + } + + // Verify admin permissions + if err := verifyAdminPermissions(ctx, c); err != nil { + return err + } + + // Get all requestable entitlements + entitlements, err := c.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ + GrantedStatus: shared.GrantedStatusAll, + AppEntitlementExpandMask: shared.AppEntitlementExpandMask{Paths: []string{"app_id", "app_resource_type_id", "app_resource_id"}}, + }) + if err != nil { + return fmt.Errorf("failed to search entitlements: %w", err) + } + if len(entitlements) == 0 { + fmt.Println("No requestable entitlements found.") + return nil + } + + // Initialize stats and get command flags + stats := &Stats{ + Total: len(entitlements), + Errors: make([]string, 0), + } + flags := getCommandFlags(v) + + fmt.Printf("Processing %d entitlements...\n", stats.Total) + + processedEntitlements := make(map[string]bool) + for i, e := range entitlements { + // Process each entitlement + if err := processEntitlement(ctx, c, e, flags, stats, processedEntitlements); err != nil { + stats.Failed++ + stats.Errors = append(stats.Errors, err.Error()) + } + + // Show progress every 10 items + if (i+1)%10 == 0 { + fmt.Printf("Processed %d/%d entitlements...\n", i+1, stats.Total) + } + } + + // Print summary + printSummary(stats) + + return nil +} + +// CommandFlags holds all the command line flags +type CommandFlags struct { + ResourceTypes []string + EntitlementIDs []string + Schema string + Format string + Separator string + Force bool + ForceNonAWS bool + SkipAWS bool + DryRun bool +} + +// getCommandFlags extracts all command line flags +func getCommandFlags(v *viper.Viper) CommandFlags { + return CommandFlags{ + ResourceTypes: v.GetStringSlice("resource-type"), + EntitlementIDs: v.GetStringSlice("entitlement-id"), + Schema: v.GetString("schema"), + Format: v.GetString("format"), + Separator: v.GetString("separator"), + Force: v.GetBool("force"), + ForceNonAWS: v.GetBool("force-non-aws"), + SkipAWS: v.GetBool("skip-aws"), + DryRun: v.GetBool("dry-run"), + } +} + +// processEntitlement handles the processing of a single entitlement +func processEntitlement(ctx context.Context, c client.C1Client, e *client.EntitlementWithBindings, flags CommandFlags, stats *Stats, processedEntitlements map[string]bool) error { + ent := e.Entitlement + if ent.DisplayName == nil || ent.AppID == nil || ent.ID == nil { + stats.Skipped++ + return nil + } + + // Apply filters + if len(flags.EntitlementIDs) > 0 && !contains(flags.EntitlementIDs, *ent.ID) { + stats.Skipped++ + return nil + } + + // Get app and resource type info + app := client.GetExpanded[shared.App](e, client.ExpandedApp) + appResourceType := client.GetExpanded[shared.AppResourceType](e, client.ExpandedAppResourceType) + if app == nil || app.DisplayName == nil || appResourceType == nil || appResourceType.DisplayName == nil { + stats.Skipped++ + return nil + } + + // Filter by resource type + if len(flags.ResourceTypes) > 0 && !contains(flags.ResourceTypes, *appResourceType.DisplayName) { + stats.Skipped++ + return nil + } + + // Clean up the display name + displayName := cleanText(*ent.DisplayName) + displayName = strings.TrimSuffix(displayName, "-access") + displayName = strings.TrimSuffix(displayName, "-permissionset") + + // Get resource name and check for generic entitlements + resourceName := getResourceName(e) + if isGenericEntitlement(*ent.DisplayName) { + displayName = resourceName + pterm.Info.Printf("Using resource name '%s' instead of generic entitlement name '%s'\n", + resourceName, *ent.DisplayName) + } + + // Generate alias + var alias string + var resourceNameGenerated string + + // Get resource name if available + if appResource := client.GetExpanded[shared.AppResource](e, client.ExpandedAppResource); appResource != nil && appResource.DisplayName != nil { + resourceNameGenerated = cleanText(*appResource.DisplayName) + } else { + resourceNameGenerated = cleanText(*app.DisplayName) + } + + // Check if we need to use resource name instead of entitlement name + if isGenericEntitlement(*ent.DisplayName) { + displayName = resourceNameGenerated + pterm.Info.Printf("Using resource name '%s' instead of generic entitlement name '%s'\n", + resourceNameGenerated, *ent.DisplayName) + } + + // Generate the alias based on schema + appName := cleanText(*app.DisplayName) + switch flags.Schema { + case "app-entitlement": + alias = generateAlias("%a-%e", flags.Separator, map[string]string{ + "a": appName, + "e": displayName, + }) + case "resource-entitlement": + alias = generateAlias("%r-%e", flags.Separator, map[string]string{ + "r": resourceNameGenerated, + "e": displayName, + }) + case "app-resource-entitlement": + alias = generateAlias("%a-%r-%e", flags.Separator, map[string]string{ + "a": appName, + "r": resourceNameGenerated, + "e": displayName, + }) + case "resource-type-entitlement": + alias = generateAlias("%t-%e", flags.Separator, map[string]string{ + "t": cleanText(*appResourceType.DisplayName), + "e": displayName, + }) + default: + alias = generateAlias(flags.Format, flags.Separator, map[string]string{ + "a": appName, + "r": resourceNameGenerated, + "t": cleanText(*appResourceType.DisplayName), + "e": displayName, + }) + } + + // Check if this is an AWS permission set + isAWSPermissionSet := strings.ToLower(*appResourceType.DisplayName) == "account" + + // Skip AWS permission sets if requested + if flags.SkipAWS && isAWSPermissionSet { + stats.Skipped++ + return nil + } + + // Skip if alias is already set and not forcing + if ent.Alias != nil && *ent.Alias != "" { + if !flags.Force && !flags.ForceNonAWS && !isAWSPermissionSet { + stats.Skipped++ + return nil + } + } + + // Skip if we've already processed this alias + if processedEntitlements[alias] { + stats.Skipped++ + return nil + } + processedEntitlements[alias] = true + + // Update the alias + if !flags.DryRun { + req := &shared.UpdateAppEntitlementRequest{ + AppEntitlement: &shared.AppEntitlementInput{ + Alias: &alias, + }, + UpdateMask: stringPtr("alias"), + } + if err := c.UpdateEntitlement(ctx, *ent.AppID, *ent.ID, req); err != nil { + return fmt.Errorf("failed to update %s: %v", *ent.DisplayName, err) + } + } + stats.Updated++ + stats.Processed++ + + return nil +} + +// verifyAdminPermissions checks if the user has admin permissions +func verifyAdminPermissions(ctx context.Context, c client.C1Client) error { + // Prompt the user + fmt.Print("Are you a super admin or app admin? (yes/no): ") + reader := bufio.NewReader(os.Stdin) + answer, _ := reader.ReadString('\n') + answer = strings.TrimSpace(strings.ToLower(answer)) + if answer != "yes" { + return fmt.Errorf("you must be a super admin or app admin to run this command") + } + + // Check actual permissions + isAdmin, err := checkAdminPermissions(ctx, c) + if err != nil { + return fmt.Errorf("failed to check admin permissions: %w", err) + } + if !isAdmin { + return fmt.Errorf("you do not have super admin or app admin permissions. Please contact your administrator") + } + + return nil +} + +// printSummary prints the final summary of the alias generation process +func printSummary(stats *Stats) { + fmt.Printf("\nSummary:\n") + fmt.Printf("Total entitlements: %d\n", stats.Total) + fmt.Printf("Processed: %d\n", stats.Processed) + fmt.Printf("Skipped: %d\n", stats.Skipped) + fmt.Printf("Updated: %d\n", stats.Updated) + fmt.Printf("Failed: %d\n", stats.Failed) + if len(stats.Errors) > 0 { + fmt.Printf("\nErrors:\n") + for _, err := range stats.Errors { + fmt.Printf("- %s\n", err) + } + } +} + +// Helper functions +func contains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +} + +func stringPtr(s string) *string { return &s } + +// generateAliasCmd creates the cobra command for alias generation +func generateAliasCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "generate-alias", + Short: "Generate aliases for entitlements in ConductorOne", + Long: `Generate aliases for entitlements in ConductorOne. +This command will: +- Generate aliases based on the selected schema +- By default, skip entitlements that already have aliases (except AWS permission sets) +- Show progress and summary statistics + +Available alias schemas (--schema): +- resource-entitlement: resource name + entitlement name (default) +- app-entitlement: app name + entitlement name +- app-resource-entitlement: app name + resource name + entitlement name +- resource-type-entitlement: resource type + entitlement name +- custom: Use a custom format string with --format + +Format placeholders: +- %a: App name +- %r: Resource name +- %t: Resource type +- %e: Entitlement name +- %s: Any separator (default: "-") + +Example formats: +- "%r-%e" (default): resource-entitlement +- "%a-%e": app-entitlement +- "%a-%r-%e": app-resource-entitlement +- "%t-%e": resource-type-entitlement + +The alias format can be customized using flags: +- --schema: Choose a predefined schema or "custom" +- --format: Custom format string (only used with schema=custom) +- --separator: Custom separator (default: "-") +- --force: Override ALL existing aliases (including AWS permission sets) +- --force-non-aws: Override existing aliases for non-AWS entitlements +- --skip-aws: Skip AWS permission sets entirely +- --dry-run: Preview changes without making them + +Filtering options: +- --resource-type: Only process entitlements with specific resource types +- --entitlement-id: Process only specific entitlements`, + RunE: generateAliasRun, + } + + // Add flags + cmd.Flags().String("schema", "resource-entitlement", "Alias schema to use (resource-entitlement, app-entitlement, app-resource-entitlement, resource-type-entitlement, custom)") + cmd.Flags().String("format", "%r-%e", "Custom format string for alias generation (only used with schema=custom)") + cmd.Flags().String("separator", "-", "Separator to use between components") + cmd.Flags().Bool("force", false, "Override ALL existing aliases (including AWS permission sets)") + cmd.Flags().Bool("force-non-aws", false, "Override existing aliases for non-AWS entitlements") + cmd.Flags().Bool("skip-aws", false, "Skip AWS permission sets entirely") + cmd.Flags().Bool("dry-run", false, "Preview changes without making them") + cmd.Flags().StringSlice("resource-type", []string{}, "Only process entitlements with these resource types") + cmd.Flags().StringSlice("entitlement-id", []string{}, "Process only these entitlements") + + // Mark flags as mutually exclusive + cmd.MarkFlagsMutuallyExclusive("force", "force-non-aws") + + return cmd +} + +// generateAlias generates an alias using the given format and values +func generateAlias(format, separator string, values map[string]string) string { + // Replace placeholders with values + result := format + for key, value := range values { + result = strings.ReplaceAll(result, "%"+key, value) + } + // Replace separator placeholder + result = strings.ReplaceAll(result, "%s", separator) + return result +} + +// checkAdminPermissions checks if the user has admin permissions +func checkAdminPermissions(ctx context.Context, c client.C1Client) (bool, error) { + // Get user's identity + userIntro, err := c.AuthIntrospect(ctx) + if err != nil { + return false, fmt.Errorf("failed to get user identity: %w", err) + } + + // Check if user is a super admin or app admin + for _, role := range userIntro.Roles { + // Check for roles that indicate super admin access + if strings.HasPrefix(role, "role/c1.api.tenant.v1.Tenant:owner") || + strings.HasPrefix(role, "role/c1.api.auth.v1.Auth:owner") || + // Check for app admin roles + strings.HasPrefix(role, "role/c1.api.app.v1.Apps:owner") || + strings.HasPrefix(role, "role/c1.api.app.v1.AppEntitlements:owner") { + return true, nil + } + } + + // Check if user is an app admin for any app + apps, err := c.ListApps(ctx) + if err != nil { + return false, fmt.Errorf("failed to list apps: %w", err) + } + + for _, app := range apps { + if app.ID == nil { + continue + } + + // Get app users + appUsers, err := c.ListAppUsers(ctx, *app.ID) + if err != nil { + continue // Skip this app if we can't get users + } + + // Check if user is an admin for this app + for _, appUser := range appUsers { + if appUser.IdentityUserID != nil && *appUser.IdentityUserID == *userIntro.UserID { + // Check if user has admin role in their profile + if profile, ok := appUser.Profile["roles"]; ok { + if roles, ok := profile.([]interface{}); ok { + for _, role := range roles { + if roleStr, ok := role.(string); ok && roleStr == "admin" { + return true, nil + } + } + } + } + } + } + } + + return false, nil +} From c1a847204a38e15d367168febc7dd2859fc6608b Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:56:00 -0600 Subject: [PATCH 05/34] Add AWS SSO profile creation in task completion handling This commit enhances the `handleWaitBehavior` function to manage AWS-specific actions upon successful entitlement grants. It checks if the entitlement is an AWS permission set and attempts to create an AWS SSO profile for the user. Additionally, it corrects a typo in the success message for entitlement revocation. --- cmd/cone/get_drop_task.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index beefbb4c..841af8ae 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -488,6 +488,9 @@ func getEntitlementDetails(ctx context.Context, c client.C1Client, v *viper.Vipe return entitlementId, appId, nil } +// handleWaitBehavior manages the waiting state for task completion and handles AWS-specific post-grant actions +// When a grant task is successful, it checks if the entitlement is an AWS permission set +// If it is, it attempts to create an AWS SSO profile for the user func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { spinner, _ := pterm.DefaultSpinner.Start("Waiting for ticket to close.") var taskItem *shared.Task @@ -517,6 +520,35 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas taskOutcome := taskItem.TaskType.TaskTypeGrant.Outcome if *taskOutcome == shared.TaskTypeGrantOutcomeGrantOutcomeGranted { spinner.Success("Entitlement granted successfully.") + + appID := *taskItem.TaskType.TaskTypeGrant.AppID + entitlementID := *taskItem.TaskType.TaskTypeGrant.AppEntitlementID + entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) + if err != nil { + return nil + } + + resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) + if err != nil { + return nil + } + + // Check if this is an AWS permission set entitlement + // If it is, create an AWS SSO profile for the user + if client.IsAWSPermissionSet(entitlement, resourceType) { + resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) + if err != nil { + return nil + } + + // Attempt to create the AWS SSO profile + // This will use the AWS SSO URL configured via 'cone config-aws set-sso-url' + if err := client.CreateAWSSSOProfile(entitlement, resource); err != nil { + spinner.Warning(fmt.Sprintf("Failed to create AWS SSO profile: %v", err)) + } else { + spinner.Success(fmt.Sprintf("Successfully created AWS SSO profile for entitlement %s", *entitlement.DisplayName)) + } + } } else { spinner.Fail(fmt.Sprintf("Failed to grant entitlement %s", string(*taskOutcome))) return fmt.Errorf("failed to grant entitlement %s", string(*taskOutcome)) @@ -525,7 +557,7 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas if taskItem.TaskType.TaskTypeRevoke != nil { taskOutcome := taskItem.TaskType.TaskTypeRevoke.Outcome if *taskOutcome == shared.TaskTypeRevokeOutcomeRevokeOutcomeRevoked { - spinner.Success("Entitlement revoked succesfully.") + spinner.Success("Entitlement revoked successfully.") } else { spinner.Fail(fmt.Sprintf("Failed to revoke entitlement %s", string(*taskOutcome))) return fmt.Errorf("failed to revoke entitlement %s", string(*taskOutcome)) From 97283b53d9ba904cb4fecbafee866a41503df4b0 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:56:21 -0600 Subject: [PATCH 06/34] Refactor CLI command initialization in Cone This commit simplifies the command initialization in the Cone CLI by consolidating the command setup into a single function, `rootCmd()`. It retains the versioning and context management while removing redundant persistent flags and command additions, streamlining the code for better maintainability. --- cmd/cone/main.go | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/cmd/cone/main.go b/cmd/cone/main.go index 9d09d497..80843995 100644 --- a/cmd/cone/main.go +++ b/cmd/cone/main.go @@ -36,44 +36,19 @@ func main() { } func runCli(ctx context.Context) int { - cliCmd := &cobra.Command{ - Use: "cone", - Short: "Cone interacts with the ConductorOne API to manage access to entitlements.", - Version: version, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - cmd.SetContext(ctx) - return nil - }, - SilenceUsage: true, - SilenceErrors: true, + cliCmd := rootCmd() + cliCmd.Version = version + cliCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + cmd.SetContext(ctx) + return nil } - cliCmd.PersistentFlags().StringP("profile", "p", "default", "The config profile to use.") - cliCmd.PersistentFlags().BoolP("non-interactive", "i", false, "Disable prompts.") - cliCmd.PersistentFlags().String("client-id", "", "Client ID") - cliCmd.PersistentFlags().String("client-secret", "", "Client secret") - cliCmd.PersistentFlags().String("api-endpoint", "", "Override the API endpoint") - cliCmd.PersistentFlags().StringP("output", "o", "table", "Output format. Valid values: table, json, json-pretty, wide.") - cliCmd.PersistentFlags().Bool("debug", false, "Enable debug logging") - err := initConfig(cliCmd) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) return 1 } - cliCmd.AddCommand(getCmd()) - cliCmd.AddCommand(dropCmd()) - cliCmd.AddCommand(whoAmICmd()) - cliCmd.AddCommand(getUserCmd()) - cliCmd.AddCommand(searchEntitlementsCmd()) - cliCmd.AddCommand(tasksCmd()) - cliCmd.AddCommand(loginCmd()) - cliCmd.AddCommand(hasCmd()) - cliCmd.AddCommand(tokenCmd()) - cliCmd.AddCommand(terraformCmd()) - cliCmd.AddCommand(decryptCredentialCmd()) - err = cliCmd.ExecuteContext(ctx) if err != nil { _, _, v, _ := cmdContext(cliCmd) From 58001061462b24d7c526b3fa16fa4c882b25b4fd Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:56:43 -0600 Subject: [PATCH 07/34] Enhance search entitlements command in Cone CLI This commit updates the `search` command in the Cone CLI to provide a more detailed description and usage instructions. It introduces a two-phase search mechanism that first attempts to find entitlements by exact alias match, followed by a query search if no matches are found. Additionally, it allows for combined searches using both alias and query, and defaults to showing all entitlements if no filters are applied. These changes improve the command's usability and functionality for users searching for entitlements in ConductorOne. --- cmd/cone/search_entitlements.go | 104 +++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 22 deletions(-) diff --git a/cmd/cone/search_entitlements.go b/cmd/cone/search_entitlements.go index 3747f351..128977f6 100644 --- a/cmd/cone/search_entitlements.go +++ b/cmd/cone/search_entitlements.go @@ -11,8 +11,20 @@ import ( func searchEntitlementsCmd() *cobra.Command { cmd := &cobra.Command{ Use: "search", - Short: "", - RunE: searchEntitlementsRun, + Short: "Search for entitlements in ConductorOne", + Long: `Search for entitlements in ConductorOne using various filters. +This command allows you to: +- Search by entitlement name or alias +- Filter by app name +- Show only granted or not granted entitlements +- Include deleted entitlements + +The search results will show: +- Whether you have access to each entitlement +- The entitlement's alias and display name +- The app it belongs to +- The resource type and resource name`, + RunE: searchEntitlementsRun, } addEntitlementAliasFlag(cmd) addQueryFlag(cmd) @@ -29,37 +41,85 @@ func searchEntitlementsRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - if err := validateArgLenth(0, args, cmd); err != nil { - return err - } query := v.GetString(queryFlag) alias := v.GetString(entitlementAliasFlag) - grantedStatus := client.GrantedStatusAll + if len(args) == 1 { + alias = args[0] + } + + grantedStatus := shared.GrantedStatusAll if v.GetBool(grantedFlag) { - grantedStatus = client.GrantedStatusGranted + grantedStatus = shared.GrantedStatusGranted } else if v.GetBool(notGrantedFlag) { - grantedStatus = client.GrantedStatusNotGranted + grantedStatus = shared.GrantedStatusNotGranted } - // TODO(morgabra) 2-phase search: Accept a positional arg: - // 1. Test if it's a direct alias - // 2. Use it as a query - entitlements, err := c.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ - Query: query, - EntitlementAlias: alias, - GrantedStatus: grantedStatus, - AppDisplayName: v.GetString(appDisplayNameFlag), - IncludeDeleted: v.GetBool(includeDeletedFlag), - AppEntitlementExpandMask: shared.AppEntitlementExpandMask{Paths: []string{"app_id", "app_resource_type_id", "app_resource_id"}}, - }) - if err != nil { - return err + // Phase 1: Try exact alias match first if an alias is provided + var entitlements []*client.EntitlementWithBindings + if alias != "" { + exactMatchEntitlements, err := c.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ + EntitlementAlias: alias, + GrantedStatus: grantedStatus, + AppDisplayName: v.GetString(appDisplayNameFlag), + IncludeDeleted: v.GetBool(includeDeletedFlag), + AppEntitlementExpandMask: shared.AppEntitlementExpandMask{Paths: []string{"app_id", "app_resource_type_id", "app_resource_id"}}, + }) + if err != nil { + return err + } + entitlements = exactMatchEntitlements + } + + // Phase 2: If no exact matches found and we have a query, try query search + if len(entitlements) == 0 && query != "" { + queryEntitlements, err := c.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ + Query: query, + GrantedStatus: grantedStatus, + AppDisplayName: v.GetString(appDisplayNameFlag), + IncludeDeleted: v.GetBool(includeDeletedFlag), + AppEntitlementExpandMask: shared.AppEntitlementExpandMask{Paths: []string{"app_id", "app_resource_type_id", "app_resource_id"}}, + }) + if err != nil { + return err + } + entitlements = queryEntitlements + } + + // If still no results and we have both alias and query, try combined search + if len(entitlements) == 0 && alias != "" && query != "" { + combinedEntitlements, err := c.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ + Query: query, + EntitlementAlias: alias, + GrantedStatus: grantedStatus, + AppDisplayName: v.GetString(appDisplayNameFlag), + IncludeDeleted: v.GetBool(includeDeletedFlag), + AppEntitlementExpandMask: shared.AppEntitlementExpandMask{Paths: []string{"app_id", "app_resource_type_id", "app_resource_id"}}, + }) + if err != nil { + return err + } + entitlements = combinedEntitlements + } + + // If no alias or query provided, show all entitlements + if len(entitlements) == 0 && alias == "" && query == "" { + allEntitlements, err := c.SearchEntitlements(ctx, &client.SearchEntitlementsFilter{ + GrantedStatus: grantedStatus, + AppDisplayName: v.GetString(appDisplayNameFlag), + IncludeDeleted: v.GetBool(includeDeletedFlag), + AppEntitlementExpandMask: shared.AppEntitlementExpandMask{Paths: []string{"app_id", "app_resource_type_id", "app_resource_id"}}, + }) + if err != nil { + return err + } + entitlements = allEntitlements } + + outputManager := output.NewManager(ctx, v) resp := &ExpandedEntitlementsResponse{ Entitlements: entitlements, } - outputManager := output.NewManager(ctx, v) err = outputManager.Output(ctx, resp) if err != nil { return err From 431b30818aff0baab6e31d79c356550e3c01b9f7 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:57:12 -0600 Subject: [PATCH 08/34] Enhance task approval process in Cone CLI This commit improves the `runApproveTasks` function by adding detailed logging for the task approval process. It retrieves task and entitlement details, checks for AWS permission sets, and attempts to create an AWS SSO profile if applicable. Warnings are logged for any failures in fetching resource type or resource details, enhancing the user experience and debugging capabilities during task approvals. --- cmd/cone/task_approve_deny.go | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index 9a442b1a..9d0f9a99 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "github.com/spf13/cobra" @@ -37,10 +38,69 @@ func denyTasksCmd() *cobra.Command { func runApproveTasks(cmd *cobra.Command, args []string) error { return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { + fmt.Printf("\nStarting task approval process for task %s\n", taskId) + + taskResp, err := c.GetTask(ctx, taskId) + if err != nil { + return nil, err + } + fmt.Printf("Got task details: %+v\n", taskResp.TaskView.Task) + + var appID, entitlementID string + if taskResp.TaskView.Task.TaskType != nil { + if taskResp.TaskView.Task.TaskType.TaskTypeGrant != nil { + appID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppID + entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppEntitlementID + } else if taskResp.TaskView.Task.TaskType.TaskTypeRevoke != nil { + appID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppID + entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppEntitlementID + } else if taskResp.TaskView.Task.TaskType.TaskTypeCertify != nil { + appID = *taskResp.TaskView.Task.TaskType.TaskTypeCertify.AppID + entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeCertify.AppEntitlementID + } + } + fmt.Printf("App ID: %s, Entitlement ID: %s\n", appID, entitlementID) + + if appID == "" || entitlementID == "" { + return nil, fmt.Errorf("could not determine app ID or entitlement ID from task") + } + + entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) + if err != nil { + return nil, err + } + fmt.Printf("Got entitlement details: %+v\n", entitlement) + approveResp, err := c.ApproveTask(ctx, taskId, comment, policyId) if err != nil { return nil, err } + fmt.Printf("Task approved successfully\n") + + resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) + if err != nil { + fmt.Printf("Warning: Failed to get resource type details: %v\n", err) + return approveResp.TaskView.Task, nil + } + fmt.Printf("Got resource type details: %+v\n", resourceType) + + if client.IsAWSPermissionSet(entitlement, resourceType) { + fmt.Printf("Detected AWS permission set, getting resource details...\n") + resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) + if err != nil { + fmt.Printf("Warning: Failed to get resource details: %v\n", err) + return approveResp.TaskView.Task, nil + } + + if err := client.CreateAWSSSOProfile(entitlement, resource); err != nil { + fmt.Printf("Warning: Failed to create AWS SSO profile: %v\n", err) + } else { + fmt.Printf("Successfully created AWS SSO profile for entitlement %s\n", *entitlement.DisplayName) + } + } else { + fmt.Printf("Not an AWS permission set\n") + } + return approveResp.TaskView.Task, nil }) } From 0e55d9bbc05ffcb91f4510335e60d1e43c188b97 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:57:30 -0600 Subject: [PATCH 09/34] Enhance task search command in Cone CLI This commit updates the `search` command in the Cone CLI to provide a more comprehensive description and usage instructions. It details the various filters available for searching tasks, including task type, state, and specific access reviews. The enhancements improve the command's usability and clarity for users searching for tasks in ConductorOne. --- cmd/cone/task_search.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/cone/task_search.go b/cmd/cone/task_search.go index 72b941ee..dc16e370 100644 --- a/cmd/cone/task_search.go +++ b/cmd/cone/task_search.go @@ -13,8 +13,21 @@ import ( func searchTasksCmd() *cobra.Command { cmd := &cobra.Command{ Use: "search", - Short: "Search for tasks using various filters", - RunE: searchTasksRun, + Short: "Search for tasks in ConductorOne", + Long: `Search for tasks in ConductorOne using various filters. +This command allows you to: +- Search by task type (grant, revoke, certify) +- Filter by task state (open, closed) +- Filter by app, entitlement, resource, or user +- Search for specific access reviews +- Include deleted tasks + +The search results will show: +- Task type and state +- Related apps, entitlements, and resources +- Assignees and subjects +- Creation and update timestamps`, + RunE: searchTasksRun, } addAccessReviewIDsFlag(cmd) From 80570b3f91938c148df5d5dca49479cb678902fb Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:57:43 -0600 Subject: [PATCH 10/34] Enhance token command in Cone CLI This commit updates the `token` command in the Cone CLI to provide a clear description and detailed usage instructions. It explains the purpose of obtaining a ConductorOne API access token using OAuth2 client credentials flow, highlighting its utility for debugging, verification, and API calls. The addition of the `--raw` flag allows users to retrieve the bearer token without formatting, improving the command's usability and clarity. --- cmd/cone/token.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cmd/cone/token.go b/cmd/cone/token.go index 3e3afa65..84799e70 100644 --- a/cmd/cone/token.go +++ b/cmd/cone/token.go @@ -18,8 +18,16 @@ type Token struct { func tokenCmd() *cobra.Command { cmd := &cobra.Command{ Use: "token", - Short: "", - RunE: tokenRun, + Short: "Get a ConductorOne API access token", + Long: `Get a ConductorOne API access token using your client credentials. +This command is useful for: +- Debugging authentication issues +- Getting a token to use with other tools +- Verifying your credentials are working correctly + +The token is obtained using OAuth2 client credentials flow and can be used to make API calls to ConductorOne. +Use --raw flag to get just the bearer token without any formatting.`, + RunE: tokenRun, } addRawTokenFlag(cmd) return cmd From 7933985d8697638c9ebb7323a66582ef1f465430 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:58:09 -0600 Subject: [PATCH 11/34] Add UpdateEntitlement method to C1Client interface This commit introduces the `UpdateEntitlement` method to the `C1Client` interface, allowing for the updating of app entitlements. This addition enhances the client functionality by enabling modifications to existing entitlements, improving the overall capability of the Cone CLI in managing app permissions. --- pkg/client/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/client/client.go b/pkg/client/client.go index 1edc0447..421ea90b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -96,6 +96,7 @@ type C1Client interface { ListAppUserCredentials(ctx context.Context, appID string, appUserID string) ([]shared.AppUserCredential, error) ListPolicies(ctx context.Context) ([]shared.Policy, error) ListEntitlements(ctx context.Context, appId string) ([]shared.AppEntitlement, error) + UpdateEntitlement(ctx context.Context, appID, entitlementID string, req *shared.UpdateAppEntitlementRequest) error } func (c *client) BaseURL() string { From 6bca30ecfd92d4bf3ae0448bf07a192ef95425ba Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:58:25 -0600 Subject: [PATCH 12/34] Add UpdateEntitlement method to client This commit implements the `UpdateEntitlement` method in the client, enabling updates to existing app entitlements. This enhancement improves the functionality of the client by allowing modifications to entitlements, thereby increasing the overall capability of the Cone CLI in managing app permissions. --- pkg/client/entitlement.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/client/entitlement.go b/pkg/client/entitlement.go index d57a7246..38bbb3cd 100644 --- a/pkg/client/entitlement.go +++ b/pkg/client/entitlement.go @@ -221,3 +221,12 @@ func (c *client) ListEntitlements(ctx context.Context, appId string) ([]shared.A return entitlements, nil } + +func (c *client) UpdateEntitlement(ctx context.Context, appID, entitlementID string, req *shared.UpdateAppEntitlementRequest) error { + _, err := c.sdk.AppEntitlements.Update(ctx, operations.C1APIAppV1AppEntitlementsUpdateRequest{ + AppID: appID, + ID: entitlementID, + UpdateAppEntitlementRequest: req, + }) + return err +} From 14a6cc0818d42b82f7125d69f51e90b971079b73 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 18:58:38 -0600 Subject: [PATCH 13/34] Add AWS SSO profile creation and permission set validation This commit introduces two new functions: `IsAWSPermissionSet` to validate AWS permission sets based on entitlement and resource type, and `CreateAWSSSOProfile` to create an AWS SSO profile for a permission set. The new functionality includes error handling for missing parameters and checks for existing profiles, enhancing the management of AWS SSO configurations within the Cone CLI. --- pkg/client/task.go | 123 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/pkg/client/task.go b/pkg/client/task.go index 0a9f54fb..0b260c17 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -2,9 +2,15 @@ package client import ( "context" + "errors" + "fmt" + "os" + "path/filepath" + "strings" "github.com/conductorone/conductorone-sdk-go/pkg/models/operations" "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" + "github.com/spf13/viper" ) func (c *client) GetTask(ctx context.Context, taskId string) (*shared.TaskServiceGetResponse, error) { @@ -158,3 +164,120 @@ func (c *client) EscalateTask(ctx context.Context, taskID string) (*shared.TaskS } return resp.TaskServiceActionResponse, nil } + +func IsAWSPermissionSet(entitlement *shared.AppEntitlement, resourceType *shared.AppResourceType) bool { + if entitlement == nil || resourceType == nil { + return false + } + + // Check resource type display name + if resourceType.DisplayName != nil { + if strings.Contains(strings.ToLower(*resourceType.DisplayName), "aws permission set") { + return true + } + } + + // Check SourceConnectorIds for AWS SSO permission set ARNs + if entitlement.SourceConnectorIds != nil { + for _, value := range entitlement.SourceConnectorIds { + if strings.Contains(value, "arn:aws:sso:::permissionSet/") { + return true + } + } + } + + return false +} + +// CreateAWSSSOProfile creates an AWS SSO profile for a permission set +func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.AppResource) error { + if entitlement == nil || resource == nil { + return errors.New("entitlement and resource are required") + } + + // Get AWS account ID and permission set ARN from sourceConnectorIds + var accountID, permissionSetARN string + for _, value := range entitlement.SourceConnectorIds { + parts := strings.Split(value, "|") + if len(parts) == 2 { + accountID = parts[0] + permissionSetARN = parts[1] + break + } + } + + if accountID == "" || permissionSetARN == "" { + return errors.New("could not find AWS account ID or permission set ARN in sourceConnectorIds") + } + + // Extract role name from entitlement display name (everything before first space) + roleName := strings.Split(*entitlement.DisplayName, " ")[0] + + // Get AWS account name from resource display name + accountName := "aws" + if resource != nil && resource.DisplayName != nil { + accountName = strings.ToLower(strings.ReplaceAll(*resource.DisplayName, " ", "-")) + } + + // Create a profile/session name based on the account name and role name + profileName := fmt.Sprintf("%s-%s", accountName, strings.ToLower(roleName)) + + // Get the SSO start URL from Viper config + ssoStartURL := viper.GetString("aws_sso_start_url") + if ssoStartURL == "" { + return errors.New("AWS SSO start URL is not configured. Please run 'cone config-aws set-sso-url '") + } + + // Create the AWS config directory if it doesn't exist + awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws") + if err := os.MkdirAll(awsConfigDir, 0700); err != nil { + return fmt.Errorf("failed to create AWS config directory: %w", err) + } + + // Read existing config file + configPath := filepath.Join(awsConfigDir, "config") + configContent, err := os.ReadFile(configPath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read AWS config file: %w", err) + } + + // Check if profile already exists + configStr := string(configContent) + if strings.Contains(configStr, fmt.Sprintf("[profile %s]", profileName)) { + return fmt.Errorf("AWS profile '%s' already exists", profileName) + } + + // Check if SSO session already exists + ssoSessionExists := strings.Contains(configStr, "[sso-session cone-sso]") + + // Create new profile configuration + newProfile := fmt.Sprintf(` +[profile %s] +credential_process = cone aws-credentials "%s" +cone_sso_account_id = %s +cone_sso_role_name = %s +cone_sso_region = us-east-1 +cone_sso_start_url = %s +cone_sso_registration_scopes = sso:account:access +sso_session = cone-sso +region = us-east-1 +output = json +`, profileName, profileName, accountID, roleName, ssoStartURL) + + // Add SSO session configuration if it doesn't exist + if !ssoSessionExists { + newProfile += fmt.Sprintf(` +[sso-session cone-sso] +sso_start_url = %s +sso_region = us-east-1 +sso_registration_scopes = sso:account:access +`, ssoStartURL) + } + + // Append new profile to config file + if err := os.WriteFile(configPath, append(configContent, []byte(newProfile)...), 0600); err != nil { + return fmt.Errorf("failed to write AWS config file: %w", err) + } + + return nil +} From 2a854d99b58f3dd5967215a7360f6c3a221d302a Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 20:53:18 -0600 Subject: [PATCH 14/34] Update warning messages for consistency in get_drop_task.go This commit modifies the wording of several warning messages in the `get_drop_task.go` file to ensure consistent capitalization and phrasing. The changes enhance the clarity and professionalism of user prompts within the Cone CLI. --- cmd/cone/get_drop_task.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index 841af8ae..eb6f3cce 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -21,9 +21,9 @@ import ( const durationErrorMessage = "grant duration must be less than or equal to max provision time" const durationInputTip = "We accept a sequence of decimal numbers, each with optional fraction and a unit suffix," + "such as \"12h\", \"1w2d\" or \"2h45m\". Valid units are (m)inutes, (h)ours, (d)ays, (w)eeks." -const justificationWarningMessage = "Please provide a justification when requesting access to an entitlement." +const justificationWarningMessage = "please provide a justification when requesting access to an entitlement" const justificationInputTip = "You can add a justification using -j or --justification" -const appUserMultipleUsersWarningMessage = "This app has multiple users. Please select any one. " +const appUserMultipleUsersWarningMessage = "this app has multiple users. Please select any one. " func getCmd() *cobra.Command { cmd := &cobra.Command{ From 4455830b7a3142cdf880b204fe25f63d363f2e71 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 21:21:16 -0600 Subject: [PATCH 15/34] Refactor AWS credentials structure and improve documentation This commit updates the `aws_credentials.go` file by correcting the capitalization of `AccessKeyId` to `AccessKeyID` for consistency. Additionally, it enhances the documentation comments throughout the file, ensuring they are complete and consistent in style. These changes improve code clarity and maintainability within the Cone CLI. --- cmd/cone/aws_credentials.go | 61 +++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/cmd/cone/aws_credentials.go b/cmd/cone/aws_credentials.go index 4c628788..be80e832 100644 --- a/cmd/cone/aws_credentials.go +++ b/cmd/cone/aws_credentials.go @@ -13,32 +13,33 @@ import ( "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" "github.com/conductorone/cone/pkg/client" + "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" ) -// AWSCredentials represents the structure of AWS temporary credentials -// that will be output in JSON format +// AWSCredentials represents the structure of AWS temporary credentials. +// that will be output in JSON format. type AWSCredentials struct { Version int `json:"Version"` - AccessKeyId string `json:"AccessKeyId"` + AccessKeyID string `json:"AccessKeyId"` SecretAccessKey string `json:"SecretAccessKey"` SessionToken string `json:"SessionToken"` Expiration string `json:"Expiration"` } -// RoleCredentialsResponse represents the response from AWS SSO get-role-credentials API +// RoleCredentialsResponse represents the response from AWS SSO get-role-credentials API. type RoleCredentialsResponse struct { RoleCredentials struct { - AccessKeyId string `json:"accessKeyId"` + AccessKeyID string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` SessionToken string `json:"sessionToken"` Expiration int64 `json:"expiration"` } `json:"roleCredentials"` } -// awsCredentialsCmd creates the cobra command for getting AWS credentials -// Usage: cone aws-credentials +// awsCredentialsCmd creates the cobra command for getting AWS credentials. +// Usage: cone aws-credentials . func awsCredentialsCmd() *cobra.Command { cmd := &cobra.Command{ Use: "aws-credentials ", @@ -48,8 +49,8 @@ func awsCredentialsCmd() *cobra.Command { return cmd } -// awsCredentialsRun is the main function that handles getting AWS credentials -// It verifies access, reads AWS config, and retrieves temporary credentials +// awsCredentialsRun is the main function that handles getting AWS credentials. +// It verifies access, reads AWS config, and retrieves temporary credentials. func awsCredentialsRun(cmd *cobra.Command, args []string) error { ctx, _, _, err := cmdContext(cmd) if err != nil { @@ -107,7 +108,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error { output := AWSCredentials{ Version: 1, - AccessKeyId: creds.AccessKeyId, + AccessKeyID: creds.AccessKeyID, SecretAccessKey: creds.SecretAccessKey, SessionToken: creds.SessionToken, Expiration: creds.Expiration, @@ -118,12 +119,12 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to marshal credentials: %w", err) } - fmt.Println(string(jsonOutput)) + pterm.Println(string(jsonOutput)) return nil } -// extractProfileConfig extracts the configuration section for a specific AWS profile -// from the AWS config file +// extractProfileConfig extracts the configuration section for a specific AWS profile. +// from the AWS config file. func extractProfileConfig(config, profileSection string) string { lines := strings.Split(config, "\n") var profileLines []string @@ -145,8 +146,8 @@ func extractProfileConfig(config, profileSection string) string { return strings.Join(profileLines, "\n") } -// extractConeSSOAccountID extracts the AWS account ID from the profile configuration -// This is used to identify which AWS account to get credentials for +// extractConeSSOAccountID extracts the AWS account ID from the profile configuration. +// This is used to identify which AWS account to get credentials for. func extractConeSSOAccountID(profileConfig string) string { for _, line := range strings.Split(profileConfig, "\n") { if strings.HasPrefix(line, "cone_sso_account_id") { @@ -159,8 +160,8 @@ func extractConeSSOAccountID(profileConfig string) string { return "" } -// extractConeSSORoleName extracts the AWS role name from the profile configuration -// This is the role that will be assumed when getting credentials +// extractConeSSORoleName extracts the AWS role name from the profile configuration. +// This is the role that will be assumed when getting credentials. func extractConeSSORoleName(profileConfig string) string { for _, line := range strings.Split(profileConfig, "\n") { if strings.HasPrefix(line, "cone_sso_role_name") { @@ -173,8 +174,8 @@ func extractConeSSORoleName(profileConfig string) string { return "" } -// extractConeSSOStartURL extracts the AWS SSO start URL from the profile configuration -// This is the URL used to initiate the SSO login process +// extractConeSSOStartURL extracts the AWS SSO start URL from the profile configuration. +// This is the URL used to initiate the SSO login process. func extractConeSSOStartURL(profileConfig string) string { for _, line := range strings.Split(profileConfig, "\n") { if strings.HasPrefix(line, "cone_sso_start_url") { @@ -187,8 +188,8 @@ func extractConeSSOStartURL(profileConfig string) string { return "" } -// extractConeSSORegion extracts the AWS region from the profile configuration -// Defaults to us-east-1 if not specified +// extractConeSSORegion extracts the AWS region from the profile configuration. +// Defaults to us-east-1 if not specified. func extractConeSSORegion(profileConfig string) string { for _, line := range strings.Split(profileConfig, "\n") { if strings.HasPrefix(line, "cone_sso_region") { @@ -201,8 +202,8 @@ func extractConeSSORegion(profileConfig string) string { return "us-east-1" // Default region } -// getSSOToken retrieves a valid SSO token from the AWS SSO cache -// It looks for a token that matches the given start URL and hasn't expired +// getSSOToken retrieves a valid SSO token from the AWS SSO cache. +// It looks for a token that matches the given start URL and hasn't expired. func getSSOToken(ssoStartURL string) (string, error) { cacheDir := filepath.Join(os.Getenv("HOME"), ".aws", "sso", "cache") files, err := os.ReadDir(cacheDir) @@ -235,8 +236,8 @@ func getSSOToken(ssoStartURL string) (string, error) { return "", fmt.Errorf("no valid SSO token found for %s", ssoStartURL) } -// getTemporaryCredentials retrieves temporary AWS credentials using AWS SSO -// It handles the SSO login process if needed and returns the credentials +// getTemporaryCredentials retrieves temporary AWS credentials using AWS SSO. +// It handles the SSO login process if needed and returns the credentials. func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error) { ssoStartURL := viper.GetString("aws_sso_start_url") if ssoStartURL == "" { @@ -283,7 +284,7 @@ func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error creds := &AWSCredentials{ Version: 1, - AccessKeyId: response.RoleCredentials.AccessKeyId, + AccessKeyID: response.RoleCredentials.AccessKeyID, SecretAccessKey: response.RoleCredentials.SecretAccessKey, SessionToken: response.RoleCredentials.SessionToken, Expiration: time.UnixMilli(response.RoleCredentials.Expiration).Format(time.RFC3339), @@ -292,8 +293,8 @@ func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error return creds, nil } -// checkC1Access verifies if the user has access to the requested AWS profile -// by checking their grants in ConductorOne +// checkC1Access verifies if the user has access to the requested AWS profile. +// by checking their grants in ConductorOne. func checkC1Access(ctx context.Context, profileName string) (bool, error) { cmd := &cobra.Command{} cmd.SetContext(ctx) @@ -340,8 +341,8 @@ func checkC1Access(ctx context.Context, profileName string) (bool, error) { return false, nil } -// verifySSOSession checks if the AWS SSO session is properly configured -// in the AWS config file +// verifySSOSession checks if the AWS SSO session is properly configured. +// in the AWS config file. func verifySSOSession(ssoStartURL, ssoRegion string) error { awsConfigDir := filepath.Join(os.Getenv("HOME"), ".aws") configPath := filepath.Join(awsConfigDir, "config") From 5dd08315e7fe52bb9f7ee937d5e356266edf928b Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 21:21:44 -0600 Subject: [PATCH 16/34] Enhance AWS SSO command functionality and improve user feedback This commit updates the AWS SSO commands in the Cone CLI by refining the command descriptions for clarity and consistency. It introduces the `pterm` package for improved user feedback, replacing standard print statements with styled output for setting and retrieving the AWS SSO start URL. Additionally, it enforces argument validation for the `set-sso-url` command, ensuring a URL is provided. These changes enhance the overall user experience and maintainability of the AWS configuration commands. --- cmd/cone/config.go | 54 ++++++++++++---------------------------------- 1 file changed, 14 insertions(+), 40 deletions(-) diff --git a/cmd/cone/config.go b/cmd/cone/config.go index c3fd1644..8a16e9f0 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -98,77 +99,50 @@ func getCredentials(v *viper.Viper) (string, string, error) { return clientId, clientSecret, nil } -// configAwsCmd creates the main AWS configuration command -// This command was renamed from 'config' to 'config-aws' to be more specific about its AWS functionality -// It provides subcommands for managing AWS SSO settings, particularly the SSO start URL -// which is required for AWS credential management and permission set operations +// configAwsCmd creates the main AWS configuration command. func configAwsCmd() *cobra.Command { cmd := &cobra.Command{ Use: "config-aws", - Short: "Manage AWS SSO configuration for ConductorOne", - Long: `Manage AWS SSO configuration for ConductorOne. -This command helps you configure AWS SSO settings needed for AWS credential management. -It allows you to set and get the AWS SSO start URL, which is required for: -- Getting AWS credentials via 'cone aws-credentials' -- Managing AWS permission sets -- Accessing AWS resources through ConductorOne`, + Short: "Configure AWS settings", } - cmd.AddCommand(setAWSSSOStartURLCmd()) cmd.AddCommand(getAWSSSOStartURLCmd()) - return cmd } -// setAWSSSOStartURLCmd creates the command for setting the AWS SSO start URL -// This command was renamed from 'set-aws-sso-url' to 'set-sso-url' for simplicity -// The URL is stored in the Viper configuration and is used by other AWS-related commands -// to authenticate with AWS SSO and manage permissions +// setAWSSSOStartURLCmd creates the command for setting the AWS SSO start URL. func setAWSSSOStartURLCmd() *cobra.Command { cmd := &cobra.Command{ Use: "set-sso-url ", - Short: "Set the AWS SSO start URL for your organization", - Long: `Set the AWS SSO start URL for your organization. -This URL is required for AWS SSO authentication and is used when: -- Getting AWS credentials via 'cone aws-credentials' -- Managing AWS permission sets -- Accessing AWS resources through ConductorOne - -Example: cone config-aws set-sso-url https://your-org.awsapps.com/start`, - Args: cobra.ExactArgs(1), + Short: "Set the AWS SSO start URL", RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("must provide a URL") + } url := args[0] viper.Set("aws_sso_start_url", url) if err := viper.WriteConfig(); err != nil { - return fmt.Errorf("failed to write config: %w", err) + return err } - fmt.Printf("AWS SSO start URL set to: %s\n", url) + pterm.Info.Printf("AWS SSO start URL set to: %s\n", url) return nil }, } return cmd } -// getAWSSSOStartURLCmd creates the command for retrieving the current AWS SSO start URL -// This command was renamed from 'get-aws-sso-url' to 'get-sso-url' for consistency -// It reads the URL from the Viper configuration and displays it to the user -// If no URL is set, it informs the user that the URL needs to be configured +// getAWSSSOStartURLCmd creates the command for retrieving the current AWS SSO start URL. func getAWSSSOStartURLCmd() *cobra.Command { cmd := &cobra.Command{ Use: "get-sso-url", - Short: "Get the currently configured AWS SSO start URL", - Long: `Get the currently configured AWS SSO start URL. -This URL is used for AWS SSO authentication and is required for: -- Getting AWS credentials via 'cone aws-credentials' -- Managing AWS permission sets -- Accessing AWS resources through ConductorOne`, + Short: "Get the current AWS SSO start URL", RunE: func(cmd *cobra.Command, args []string) error { url := viper.GetString("aws_sso_start_url") if url == "" { - fmt.Println("AWS SSO start URL is not set") + pterm.Warning.Println("AWS SSO start URL is not set") return nil } - fmt.Printf("AWS SSO start URL: %s\n", url) + pterm.Info.Printf("AWS SSO start URL: %s\n", url) return nil }, } From 6101bc697feb927a45ae46818bb9a118cb59a8a9 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 21:21:52 -0600 Subject: [PATCH 17/34] Refactor comments and enhance user feedback in alias generation This commit improves the documentation comments in the `generate_alias.go` file by ensuring consistency in punctuation and clarity. Additionally, it replaces standard print statements with styled output using the `pterm` package for better user feedback during the alias generation process. These changes enhance code readability and user experience within the Cone CLI. --- cmd/cone/generate_alias.go | 59 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/cmd/cone/generate_alias.go b/cmd/cone/generate_alias.go index a81f27b6..2ba9dda4 100644 --- a/cmd/cone/generate_alias.go +++ b/cmd/cone/generate_alias.go @@ -15,7 +15,7 @@ import ( "github.com/spf13/viper" ) -// Stats tracks the progress of alias generation +// Stats tracks the progress of alias generation. type Stats struct { Total int Processed int @@ -25,7 +25,7 @@ type Stats struct { Errors []string } -// Generic entitlement words that should be replaced with resource names +// Generic entitlement words that should be replaced with resource names. var genericEntitlementWords = []string{ "member", "assignment", @@ -34,7 +34,7 @@ var genericEntitlementWords = []string{ "group", } -// Words to remove from display names +// Words to remove from display names. var wordsToRemove = []string{ "Role member", "role member", "RoleMember", "roleMember", "Group member", "group member", "GroupMember", "groupMember", @@ -49,7 +49,7 @@ var wordsToRemove = []string{ // 3. Converting to lowercase // 4. Replacing spaces with hyphens // 5. Removing invalid characters -// 6. Ensuring proper length and format +// 6. Ensuring proper length and format. func cleanText(text string) string { // Remove anything in parentheses text = regexp.MustCompile(`\s*\([^)]*\)`).ReplaceAllString(text, "") @@ -89,7 +89,7 @@ func cleanText(text string) string { return text } -// isGenericEntitlement checks if an entitlement name is too generic to be useful +// isGenericEntitlement checks if an entitlement name is too generic to be useful. func isGenericEntitlement(name string) bool { name = strings.ToLower(strings.TrimSpace(name)) for _, word := range genericEntitlementWords { @@ -100,7 +100,7 @@ func isGenericEntitlement(name string) bool { return false } -// getResourceName extracts and cleans the resource name from an entitlement +// getResourceName extracts and cleans the resource name from an entitlement. func getResourceName(e *client.EntitlementWithBindings) string { if appResource := client.GetExpanded[shared.AppResource](e, client.ExpandedAppResource); appResource != nil && appResource.DisplayName != nil { return cleanText(*appResource.DisplayName) @@ -112,7 +112,7 @@ func getResourceName(e *client.EntitlementWithBindings) string { return "resource" // Final fallback } -// generateAliasRun is the main function that handles alias generation +// generateAliasRun is the main function that handles alias generation. func generateAliasRun(cmd *cobra.Command, args []string) error { ctx, c, v, err := cmdContext(cmd) if err != nil { @@ -133,7 +133,7 @@ func generateAliasRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to search entitlements: %w", err) } if len(entitlements) == 0 { - fmt.Println("No requestable entitlements found.") + pterm.Warning.Println("No requestable entitlements found.") return nil } @@ -144,7 +144,7 @@ func generateAliasRun(cmd *cobra.Command, args []string) error { } flags := getCommandFlags(v) - fmt.Printf("Processing %d entitlements...\n", stats.Total) + pterm.Info.Printf("Processing %d entitlements...\n", stats.Total) processedEntitlements := make(map[string]bool) for i, e := range entitlements { @@ -156,7 +156,7 @@ func generateAliasRun(cmd *cobra.Command, args []string) error { // Show progress every 10 items if (i+1)%10 == 0 { - fmt.Printf("Processed %d/%d entitlements...\n", i+1, stats.Total) + pterm.Info.Printf("Processed %d/%d entitlements...\n", i+1, stats.Total) } } @@ -166,7 +166,7 @@ func generateAliasRun(cmd *cobra.Command, args []string) error { return nil } -// CommandFlags holds all the command line flags +// CommandFlags holds all the command line flags. type CommandFlags struct { ResourceTypes []string EntitlementIDs []string @@ -179,7 +179,7 @@ type CommandFlags struct { DryRun bool } -// getCommandFlags extracts all command line flags +// getCommandFlags extracts all command line flags. func getCommandFlags(v *viper.Viper) CommandFlags { return CommandFlags{ ResourceTypes: v.GetStringSlice("resource-type"), @@ -194,7 +194,7 @@ func getCommandFlags(v *viper.Viper) CommandFlags { } } -// processEntitlement handles the processing of a single entitlement +// processEntitlement handles the processing of a single entitlement. func processEntitlement(ctx context.Context, c client.C1Client, e *client.EntitlementWithBindings, flags CommandFlags, stats *Stats, processedEntitlements map[string]bool) error { ent := e.Entitlement if ent.DisplayName == nil || ent.AppID == nil || ent.ID == nil { @@ -319,7 +319,7 @@ func processEntitlement(ctx context.Context, c client.C1Client, e *client.Entitl UpdateMask: stringPtr("alias"), } if err := c.UpdateEntitlement(ctx, *ent.AppID, *ent.ID, req); err != nil { - return fmt.Errorf("failed to update %s: %v", *ent.DisplayName, err) + return fmt.Errorf("failed to update %s: %w", *ent.DisplayName, err) } } stats.Updated++ @@ -328,10 +328,10 @@ func processEntitlement(ctx context.Context, c client.C1Client, e *client.Entitl return nil } -// verifyAdminPermissions checks if the user has admin permissions +// verifyAdminPermissions checks if the user has admin permissions. func verifyAdminPermissions(ctx context.Context, c client.C1Client) error { // Prompt the user - fmt.Print("Are you a super admin or app admin? (yes/no): ") + pterm.Info.Print("Are you a super admin or app admin? (yes/no): ") reader := bufio.NewReader(os.Stdin) answer, _ := reader.ReadString('\n') answer = strings.TrimSpace(strings.ToLower(answer)) @@ -351,23 +351,24 @@ func verifyAdminPermissions(ctx context.Context, c client.C1Client) error { return nil } -// printSummary prints the final summary of the alias generation process +// printSummary prints the final summary of the alias generation process. func printSummary(stats *Stats) { - fmt.Printf("\nSummary:\n") - fmt.Printf("Total entitlements: %d\n", stats.Total) - fmt.Printf("Processed: %d\n", stats.Processed) - fmt.Printf("Skipped: %d\n", stats.Skipped) - fmt.Printf("Updated: %d\n", stats.Updated) - fmt.Printf("Failed: %d\n", stats.Failed) + pterm.Info.Printf("\nSummary:\n") + pterm.Info.Printf("Total entitlements: %d\n", stats.Total) + pterm.Info.Printf("Processed: %d\n", stats.Processed) + pterm.Info.Printf("Skipped: %d\n", stats.Skipped) + pterm.Info.Printf("Updated: %d\n", stats.Updated) + pterm.Info.Printf("Failed: %d\n", stats.Failed) + if len(stats.Errors) > 0 { - fmt.Printf("\nErrors:\n") + pterm.Error.Printf("\nErrors:\n") for _, err := range stats.Errors { - fmt.Printf("- %s\n", err) + pterm.Error.Printf("- %s\n", err) } } } -// Helper functions +// Helper functions. func contains(slice []string, str string) bool { for _, s := range slice { if s == str { @@ -379,7 +380,7 @@ func contains(slice []string, str string) bool { func stringPtr(s string) *string { return &s } -// generateAliasCmd creates the cobra command for alias generation +// generateAliasCmd creates the cobra command for alias generation. func generateAliasCmd() *cobra.Command { cmd := &cobra.Command{ Use: "generate-alias", @@ -442,7 +443,7 @@ Filtering options: return cmd } -// generateAlias generates an alias using the given format and values +// generateAlias generates an alias using the given format and values. func generateAlias(format, separator string, values map[string]string) string { // Replace placeholders with values result := format @@ -454,7 +455,7 @@ func generateAlias(format, separator string, values map[string]string) string { return result } -// checkAdminPermissions checks if the user has admin permissions +// checkAdminPermissions checks if the user has admin permissions. func checkAdminPermissions(ctx context.Context, c client.C1Client) (bool, error) { // Get user's identity userIntro, err := c.AuthIntrospect(ctx) From ba504e2c336936d4faa8898b3b1f69fbe2c0365b Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 21:22:08 -0600 Subject: [PATCH 18/34] Refactor error handling and improve documentation in get_drop_task.go This commit enhances the error handling in the `handleWaitBehavior` function by returning errors instead of nil for failed operations. Additionally, it improves the documentation comments for clarity and consistency, ensuring that the purpose and behavior of the function are clearly communicated. These changes enhance code reliability and maintainability within the Cone CLI. --- cmd/cone/get_drop_task.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index eb6f3cce..b04e6b2b 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -488,9 +488,9 @@ func getEntitlementDetails(ctx context.Context, c client.C1Client, v *viper.Vipe return entitlementId, appId, nil } -// handleWaitBehavior manages the waiting state for task completion and handles AWS-specific post-grant actions -// When a grant task is successful, it checks if the entitlement is an AWS permission set -// If it is, it attempts to create an AWS SSO profile for the user +// handleWaitBehavior manages the waiting state for task completion and handles AWS-specific post-grant actions. +// When a grant task is successful, it checks if the entitlement is an AWS permission set. +// If it is, it attempts to create an AWS SSO profile for the user. func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { spinner, _ := pterm.DefaultSpinner.Start("Waiting for ticket to close.") var taskItem *shared.Task @@ -525,12 +525,12 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas entitlementID := *taskItem.TaskType.TaskTypeGrant.AppEntitlementID entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) if err != nil { - return nil + return err } resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) if err != nil { - return nil + return err } // Check if this is an AWS permission set entitlement @@ -538,7 +538,7 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas if client.IsAWSPermissionSet(entitlement, resourceType) { resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) if err != nil { - return nil + return err } // Attempt to create the AWS SSO profile From 2c191c1b8bc02783d0e6899e501c63aa35bcd6b3 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 21:22:17 -0600 Subject: [PATCH 19/34] Refactor task approval process to enhance user feedback and improve logging This commit updates the `task_approve_deny.go` file by replacing standard print statements with styled output using the `pterm` package for better user feedback during the task approval process. It also refactors the task type handling logic for clarity and introduces improved logging for various stages of the approval workflow. These changes enhance the overall user experience and maintainability of the task management functionality within the Cone CLI. --- cmd/cone/task_approve_deny.go | 47 ++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index 9d0f9a99..eb3a0174 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" @@ -38,28 +39,28 @@ func denyTasksCmd() *cobra.Command { func runApproveTasks(cmd *cobra.Command, args []string) error { return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { - fmt.Printf("\nStarting task approval process for task %s\n", taskId) + pterm.Info.Printf("Starting task approval process for task %s\n", taskId) taskResp, err := c.GetTask(ctx, taskId) if err != nil { return nil, err } - fmt.Printf("Got task details: %+v\n", taskResp.TaskView.Task) + pterm.Debug.Printf("Got task details: %+v\n", taskResp.TaskView.Task) var appID, entitlementID string - if taskResp.TaskView.Task.TaskType != nil { - if taskResp.TaskView.Task.TaskType.TaskTypeGrant != nil { - appID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppID - entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppEntitlementID - } else if taskResp.TaskView.Task.TaskType.TaskTypeRevoke != nil { - appID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppID - entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppEntitlementID - } else if taskResp.TaskView.Task.TaskType.TaskTypeCertify != nil { - appID = *taskResp.TaskView.Task.TaskType.TaskTypeCertify.AppID - entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeCertify.AppEntitlementID - } + switch { + case taskResp.TaskView.Task.TaskType.TaskTypeGrant != nil: + // Handle grant task + appID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppID + entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppEntitlementID + case taskResp.TaskView.Task.TaskType.TaskTypeRevoke != nil: + // Handle revoke task + appID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppID + entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppEntitlementID + default: + return nil, fmt.Errorf("unsupported task type") } - fmt.Printf("App ID: %s, Entitlement ID: %s\n", appID, entitlementID) + pterm.Debug.Printf("App ID: %s, Entitlement ID: %s\n", appID, entitlementID) if appID == "" || entitlementID == "" { return nil, fmt.Errorf("could not determine app ID or entitlement ID from task") @@ -69,36 +70,36 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { if err != nil { return nil, err } - fmt.Printf("Got entitlement details: %+v\n", entitlement) + pterm.Debug.Printf("Got entitlement details: %+v\n", entitlement) approveResp, err := c.ApproveTask(ctx, taskId, comment, policyId) if err != nil { return nil, err } - fmt.Printf("Task approved successfully\n") + pterm.Success.Println("Task approved successfully") resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) if err != nil { - fmt.Printf("Warning: Failed to get resource type details: %v\n", err) + pterm.Warning.Printf("Failed to get resource type details: %v\n", err) return approveResp.TaskView.Task, nil } - fmt.Printf("Got resource type details: %+v\n", resourceType) + pterm.Debug.Printf("Got resource type details: %+v\n", resourceType) if client.IsAWSPermissionSet(entitlement, resourceType) { - fmt.Printf("Detected AWS permission set, getting resource details...\n") + pterm.Info.Println("Detected AWS permission set, getting resource details...") resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) if err != nil { - fmt.Printf("Warning: Failed to get resource details: %v\n", err) + pterm.Warning.Printf("Failed to get resource details: %v\n", err) return approveResp.TaskView.Task, nil } if err := client.CreateAWSSSOProfile(entitlement, resource); err != nil { - fmt.Printf("Warning: Failed to create AWS SSO profile: %v\n", err) + pterm.Warning.Printf("Failed to create AWS SSO profile: %v\n", err) } else { - fmt.Printf("Successfully created AWS SSO profile for entitlement %s\n", *entitlement.DisplayName) + pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", *entitlement.DisplayName) } } else { - fmt.Printf("Not an AWS permission set\n") + pterm.Debug.Println("Not an AWS permission set") } return approveResp.TaskView.Task, nil From 1b323192359165d581893b8a58bbe6e1f5477fb4 Mon Sep 17 00:00:00 2001 From: Ali Date: Sun, 18 May 2025 21:22:23 -0600 Subject: [PATCH 20/34] Improve documentation comments in task.go for clarity and consistency This commit updates the documentation comments in the `task.go` file, specifically enhancing the comment for the `CreateAWSSSOProfile` function by adding a period for consistency. Additionally, it refines the conditional check for `resource.DisplayName` to ensure it is not nil before accessing it. These changes improve code readability and maintainability within the Cone CLI. --- pkg/client/task.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/client/task.go b/pkg/client/task.go index 0b260c17..2562dcdb 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -189,7 +189,7 @@ func IsAWSPermissionSet(entitlement *shared.AppEntitlement, resourceType *shared return false } -// CreateAWSSSOProfile creates an AWS SSO profile for a permission set +// CreateAWSSSOProfile creates an AWS SSO profile for a permission set. func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.AppResource) error { if entitlement == nil || resource == nil { return errors.New("entitlement and resource are required") @@ -215,7 +215,7 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap // Get AWS account name from resource display name accountName := "aws" - if resource != nil && resource.DisplayName != nil { + if resource.DisplayName != nil { accountName = strings.ToLower(strings.ReplaceAll(*resource.DisplayName, " ", "-")) } From c4e4c810a8c56d574e2030c0cacaad45531d9598 Mon Sep 17 00:00:00 2001 From: Ali Date: Mon, 19 May 2025 11:16:00 -0600 Subject: [PATCH 21/34] Remove unused task command registrations from the Cone CLI root command. This cleanup enhances the command structure by eliminating unnecessary commands, improving maintainability and clarity in the command hierarchy. --- cmd/cone/cmd.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cmd/cone/cmd.go b/cmd/cone/cmd.go index 568f1e0e..3c1a91f6 100644 --- a/cmd/cone/cmd.go +++ b/cmd/cone/cmd.go @@ -63,11 +63,6 @@ func rootCmd() *cobra.Command { cmd.AddCommand(getCmd()) cmd.AddCommand(dropCmd()) - cmd.AddCommand(approveTasksCmd()) - cmd.AddCommand(denyTasksCmd()) - cmd.AddCommand(getTasksCmd()) - cmd.AddCommand(tasksCommentCmd()) - cmd.AddCommand(escalateTasksCmd()) cmd.AddCommand(configAwsCmd()) cmd.AddCommand(whoAmICmd()) cmd.AddCommand(getUserCmd()) From a76db56e156de8c966c5c659f3411fa386f7420c Mon Sep 17 00:00:00 2001 From: Ali Date: Mon, 19 May 2025 11:16:34 -0600 Subject: [PATCH 22/34] Add AWS SSO profile creation and task wait handling This commit introduces the `createAWSSSOProfileIfNeeded` function to create an AWS SSO profile when an entitlement is an AWS permission set. Additionally, it refactors the `handleWaitBehavior` function to manage task completion states more effectively, providing user feedback on the outcome of entitlement requests. These enhancements improve the overall functionality and user experience within the Cone CLI. --- cmd/cone/get_drop_task.go | 192 ++++++++++++++++---------------------- 1 file changed, 79 insertions(+), 113 deletions(-) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index b04e6b2b..12f8945b 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -282,6 +282,80 @@ func printExtraTaskDetails(ctx context.Context, v *viper.Viper, c client.C1Clien return nil } +// createAWSSSOProfileIfNeeded checks if the entitlement is an AWS permission set and creates the profile if needed. +func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { + if task.TaskType.TaskTypeGrant == nil { + return nil + } + + appID := *task.TaskType.TaskTypeGrant.AppID + entitlementID := *task.TaskType.TaskTypeGrant.AppEntitlementID + + // Get the entitlement details + entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) + if err != nil { + return fmt.Errorf("failed to get entitlement details: %w", err) + } + + // Get the resource type + resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) + if err != nil { + return fmt.Errorf("failed to get resource type: %w", err) + } + + // Check if this is an AWS permission set + if client.IsAWSPermissionSet(entitlement, resourceType) { + // Get the resource details + resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) + if err != nil { + return fmt.Errorf("failed to get resource details: %w", err) + } + + if err := client.CreateAWSSSOProfile(entitlement, resource); err != nil { + return fmt.Errorf("failed to create AWS SSO profile: %w", err) + } + } + return nil +} + +// handleWaitBehavior manages the waiting state for task completion. +func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { + spinner, _ := pterm.DefaultSpinner.Start("Waiting for task to complete...") + defer spinner.Stop() + + for { + updatedTask, err := c.GetTask(ctx, *task.ID) + if err != nil { + return err + } + + if *updatedTask.TaskView.Task.State == shared.TaskStateTaskStateClosed { + if updatedTask.TaskView.Task.TaskType.TaskTypeGrant != nil { + taskOutcome := updatedTask.TaskView.Task.TaskType.TaskTypeGrant.Outcome + if taskOutcome == nil { + return fmt.Errorf("task closed but no outcome provided") + } + + if *taskOutcome == shared.TaskTypeGrantOutcomeGrantOutcomeGranted { + spinner.Success("Entitlement granted successfully.") + } else if *taskOutcome == shared.TaskTypeGrantOutcomeGrantOutcomeDenied { + spinner.Fail("Entitlement request was denied.") + return fmt.Errorf("entitlement request was denied") + } else { + spinner.Fail(fmt.Sprintf("Task completed with unexpected outcome: %s", *taskOutcome)) + return fmt.Errorf("task completed with unexpected outcome: %s", *taskOutcome) + } + } + break + } + + time.Sleep(2 * time.Second) + } + + return nil +} + +// runTask executes the task and handles the response. func runTask( cmd *cobra.Command, args []string, @@ -304,46 +378,11 @@ func runTask( return err } - userID := client.StringFromPtr(resp.UserID) - - forceCreate := v.GetBool(forceFlag) - if !forceCreate { - grants, err := c.GetGrantsForIdentity(ctx, appId, entitlementId, userID) - if err != nil { - return err - } - grantCount := 0 - for _, grant := range grants { - // We only want to check if user has a grant - if client.StringFromPtr(grant.AppEntitlementID) != "" { - grantCount++ - } - } - - // If this is get, and they have grants, just exit - if cmd.Name() == getCmd().Name() && grantCount > 0 { - pterm.Println("You already have access to this entitlement. Use --force to override this check.") - return nil - } - - if cmd.Name() == dropCmd().Name() && grantCount == 0 { - pterm.Println("You do not have existing grants to drop for this entitlement. Use --force to override this check.") - return nil - } - } - task, err := run(c, ctx, appId, entitlementId, client.StringFromPtr(resp.UserID), justification) if err != nil { return err } - if v.GetBool(extraDetailsFlag) { - err = printExtraTaskDetails(ctx, v, c, appId, entitlementId) - if err != nil { - return err - } - } - outputManager := output.NewManager(ctx, v) taskResp := Task{task: task, client: c} err = outputManager.Output(ctx, &taskResp, output.WithTransposeTable()) @@ -351,6 +390,11 @@ func runTask( return err } + // Create AWS SSO profile immediately after task creation + if err := createAWSSSOProfileIfNeeded(ctx, c, task, outputManager); err != nil { + return err + } + if wait, _ := cmd.Flags().GetBool("wait"); wait { err = handleWaitBehavior(ctx, c, task, outputManager) if err != nil { @@ -488,84 +532,6 @@ func getEntitlementDetails(ctx context.Context, c client.C1Client, v *viper.Vipe return entitlementId, appId, nil } -// handleWaitBehavior manages the waiting state for task completion and handles AWS-specific post-grant actions. -// When a grant task is successful, it checks if the entitlement is an AWS permission set. -// If it is, it attempts to create an AWS SSO profile for the user. -func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { - spinner, _ := pterm.DefaultSpinner.Start("Waiting for ticket to close.") - var taskItem *shared.Task - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(1 * time.Second): - } - task, err := c.GetTask(ctx, client.StringFromPtr(task.ID)) - if err != nil { - return err - } - - taskItem = task.TaskView.Task - taskResp := Task{task: taskItem, client: c} - err = outputManager.Output(ctx, &taskResp, output.WithTransposeTable()) - if err != nil { - return err - } - - if *taskItem.State == shared.TaskStateTaskStateClosed { - break - } - } - if taskItem.TaskType.TaskTypeGrant != nil { - taskOutcome := taskItem.TaskType.TaskTypeGrant.Outcome - if *taskOutcome == shared.TaskTypeGrantOutcomeGrantOutcomeGranted { - spinner.Success("Entitlement granted successfully.") - - appID := *taskItem.TaskType.TaskTypeGrant.AppID - entitlementID := *taskItem.TaskType.TaskTypeGrant.AppEntitlementID - entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) - if err != nil { - return err - } - - resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) - if err != nil { - return err - } - - // Check if this is an AWS permission set entitlement - // If it is, create an AWS SSO profile for the user - if client.IsAWSPermissionSet(entitlement, resourceType) { - resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) - if err != nil { - return err - } - - // Attempt to create the AWS SSO profile - // This will use the AWS SSO URL configured via 'cone config-aws set-sso-url' - if err := client.CreateAWSSSOProfile(entitlement, resource); err != nil { - spinner.Warning(fmt.Sprintf("Failed to create AWS SSO profile: %v", err)) - } else { - spinner.Success(fmt.Sprintf("Successfully created AWS SSO profile for entitlement %s", *entitlement.DisplayName)) - } - } - } else { - spinner.Fail(fmt.Sprintf("Failed to grant entitlement %s", string(*taskOutcome))) - return fmt.Errorf("failed to grant entitlement %s", string(*taskOutcome)) - } - } - if taskItem.TaskType.TaskTypeRevoke != nil { - taskOutcome := taskItem.TaskType.TaskTypeRevoke.Outcome - if *taskOutcome == shared.TaskTypeRevokeOutcomeRevokeOutcomeRevoked { - spinner.Success("Entitlement revoked successfully.") - } else { - spinner.Fail(fmt.Sprintf("Failed to revoke entitlement %s", string(*taskOutcome))) - return fmt.Errorf("failed to revoke entitlement %s", string(*taskOutcome)) - } - } - return nil -} - var processStateToString = map[shared.Processing]string{ "TASK_PROCESSING_TYPE_UNSPECIFIED": "Unknown Processing", "TASK_PROCESSING_TYPE_PROCESSING": "Processing", From f4a55cfaeacfe185ce0db23fdb32d6780d5f4dec Mon Sep 17 00:00:00 2001 From: Ali Date: Mon, 19 May 2025 11:16:45 -0600 Subject: [PATCH 23/34] Enhance AWS SSO profile creation feedback with styled output This commit updates the `CreateAWSSSOProfile` function to replace standard error messages with styled output using the `pterm` package, improving user feedback when an AWS profile already exists. Additionally, it adds a success message upon successful profile creation, enhancing the overall user experience within the Cone CLI. --- pkg/client/task.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/client/task.go b/pkg/client/task.go index 2562dcdb..0ec7e4e3 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -10,6 +10,7 @@ import ( "github.com/conductorone/conductorone-sdk-go/pkg/models/operations" "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" + "github.com/pterm/pterm" "github.com/spf13/viper" ) @@ -244,7 +245,8 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap // Check if profile already exists configStr := string(configContent) if strings.Contains(configStr, fmt.Sprintf("[profile %s]", profileName)) { - return fmt.Errorf("AWS profile '%s' already exists", profileName) + pterm.Info.Printf("AWS profile '%s' already exists\n", profileName) + return nil } // Check if SSO session already exists @@ -279,5 +281,6 @@ sso_registration_scopes = sso:account:access return fmt.Errorf("failed to write AWS config file: %w", err) } + pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", *entitlement.DisplayName) return nil } From 1a20f16e3d105249ec2022b09a9950a31e11fd13 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 20 May 2025 13:00:53 -0600 Subject: [PATCH 24/34] Add AWS configuration commands for integration mode and display settings This commit introduces new commands to manage AWS integration mode within the Cone CLI. It adds `set-integration-mode` and `get-integration-mode` commands for setting and retrieving the current integration mode (cone or native). Additionally, a `show` command is implemented to display all AWS configuration settings, including the SSO start URL and integration mode, enhancing user feedback and configurability in the CLI. --- cmd/cone/config.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/cmd/cone/config.go b/cmd/cone/config.go index 8a16e9f0..87284f90 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -99,6 +99,43 @@ func getCredentials(v *viper.Viper) (string, string, error) { return clientId, clientSecret, nil } +// showAWSConfigCmd creates the command for displaying all AWS configuration settings. +func showAWSConfigCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "show", + Short: "Show all AWS configuration settings", + RunE: func(cmd *cobra.Command, args []string) error { + // Get SSO start URL + ssoStartURL := viper.GetString("aws_sso_start_url") + if ssoStartURL == "" { + pterm.Warning.Println("AWS SSO start URL is not set") + } else { + pterm.Info.Printf("AWS SSO start URL: %s\n", ssoStartURL) + } + + // Get integration mode + integrationMode := viper.GetString("aws_integration_mode") + if integrationMode == "" { + integrationMode = "cone" // Default to cone if not set + } + pterm.Info.Printf("AWS integration mode: %s\n", integrationMode) + + // Show default behavior + pterm.Println("\nDefault behavior:") + if integrationMode == "cone" { + pterm.Println("- AWS profiles will be created automatically") + pterm.Println("- Uses Cone's credential process for authentication") + } else { + pterm.Println("- No AWS profiles will be created") + pterm.Println("- Uses AWS CLI's native SSO integration") + } + + return nil + }, + } + return cmd +} + // configAwsCmd creates the main AWS configuration command. func configAwsCmd() *cobra.Command { cmd := &cobra.Command{ @@ -107,6 +144,9 @@ func configAwsCmd() *cobra.Command { } cmd.AddCommand(setAWSSSOStartURLCmd()) cmd.AddCommand(getAWSSSOStartURLCmd()) + cmd.AddCommand(setAWSIntegrationModeCmd()) + cmd.AddCommand(getAWSIntegrationModeCmd()) + cmd.AddCommand(showAWSConfigCmd()) return cmd } @@ -148,3 +188,44 @@ func getAWSSSOStartURLCmd() *cobra.Command { } return cmd } + +// setAWSIntegrationModeCmd creates the command for setting the AWS integration mode. +func setAWSIntegrationModeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "set-integration-mode ", + Short: "Set the AWS integration mode (cone|native)", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("must provide a mode (cone|native)") + } + mode := strings.ToLower(args[0]) + if mode != "cone" && mode != "native" { + return fmt.Errorf("mode must be either 'cone' or 'native'") + } + viper.Set("aws_integration_mode", mode) + if err := viper.WriteConfig(); err != nil { + return err + } + pterm.Info.Printf("AWS integration mode set to: %s\n", mode) + return nil + }, + } + return cmd +} + +// getAWSIntegrationModeCmd creates the command for retrieving the current AWS integration mode. +func getAWSIntegrationModeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get-integration-mode", + Short: "Get the current AWS integration mode", + RunE: func(cmd *cobra.Command, args []string) error { + mode := viper.GetString("aws_integration_mode") + if mode == "" { + mode = "cone" // Default to cone if not set + } + pterm.Info.Printf("AWS integration mode: %s\n", mode) + return nil + }, + } + return cmd +} From bc5ebfd028241f788e9674145f6f9fd6c431d9d3 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 20 May 2025 13:01:04 -0600 Subject: [PATCH 25/34] Add integration mode check in CreateAWSSSOProfile function This commit introduces a check for the AWS integration mode within the CreateAWSSSOProfile function. If the integration mode is set to "native," the function will return early without creating a profile. The default mode is set to "cone" if not specified, enhancing the flexibility and configurability of AWS SSO profile creation in the Cone CLI. --- pkg/client/task.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/client/task.go b/pkg/client/task.go index 0ec7e4e3..e732bdd6 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -196,6 +196,17 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap return errors.New("entitlement and resource are required") } + // Check integration mode + integrationMode := viper.GetString("aws_integration_mode") + if integrationMode == "" { + integrationMode = "cone" // Default to cone if not set + } + + if integrationMode == "native" { + // In native mode, we don't create profiles + return nil + } + // Get AWS account ID and permission set ARN from sourceConnectorIds var accountID, permissionSetARN string for _, value := range entitlement.SourceConnectorIds { From 7254b804642d7757108d2affdc857aa9134cb5e6 Mon Sep 17 00:00:00 2001 From: Ali Date: Wed, 21 May 2025 11:22:30 -0600 Subject: [PATCH 26/34] Add raw flag to show AWS configuration command and update default integration mode This commit introduces a new `--raw` flag to the `show` command, allowing users to display the raw YAML content of the AWS configuration file. Additionally, the default integration mode is changed from "cone" to "native" for improved clarity and consistency in the CLI's behavior. These enhancements provide users with more flexibility and better feedback regarding their AWS configuration settings. --- cmd/cone/config.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cmd/cone/config.go b/cmd/cone/config.go index 87284f90..ca032fca 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -105,6 +105,23 @@ func showAWSConfigCmd() *cobra.Command { Use: "show", Short: "Show all AWS configuration settings", RunE: func(cmd *cobra.Command, args []string) error { + raw, _ := cmd.Flags().GetBool("raw") + if raw { + // Get the config file path + configFile := viper.ConfigFileUsed() + if configFile == "" { + return fmt.Errorf("no config file found") + } + + // Read and print the raw YAML content + content, err := os.ReadFile(configFile) + if err != nil { + return fmt.Errorf("error reading config file: %w", err) + } + fmt.Println(string(content)) + return nil + } + // Get SSO start URL ssoStartURL := viper.GetString("aws_sso_start_url") if ssoStartURL == "" { @@ -116,7 +133,7 @@ func showAWSConfigCmd() *cobra.Command { // Get integration mode integrationMode := viper.GetString("aws_integration_mode") if integrationMode == "" { - integrationMode = "cone" // Default to cone if not set + integrationMode = "native" // Default to native if not set } pterm.Info.Printf("AWS integration mode: %s\n", integrationMode) @@ -133,6 +150,9 @@ func showAWSConfigCmd() *cobra.Command { return nil }, } + + // Add the raw flag + cmd.Flags().Bool("raw", false, "Show raw YAML content of the config file") return cmd } @@ -221,7 +241,7 @@ func getAWSIntegrationModeCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { mode := viper.GetString("aws_integration_mode") if mode == "" { - mode = "cone" // Default to cone if not set + mode = "native" // Default to native if not set } pterm.Info.Printf("AWS integration mode: %s\n", mode) return nil From 4f55bd05a8a8613c3a3182f94199a178a116687a Mon Sep 17 00:00:00 2001 From: Ali Date: Wed, 21 May 2025 11:22:37 -0600 Subject: [PATCH 27/34] Update default AWS integration mode to 'native' in CreateAWSSSOProfile function This commit changes the default integration mode from 'cone' to 'native' in the CreateAWSSSOProfile function, enhancing clarity and consistency in the AWS SSO profile creation process. This adjustment aligns with recent updates to improve user experience and configurability within the Cone CLI. --- pkg/client/task.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/task.go b/pkg/client/task.go index e732bdd6..34d1a118 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -199,7 +199,7 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap // Check integration mode integrationMode := viper.GetString("aws_integration_mode") if integrationMode == "" { - integrationMode = "cone" // Default to cone if not set + integrationMode = "native" // Default to native if not set } if integrationMode == "native" { From e883311531ae82655343479d79144b21d3079d93 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Mon, 11 Aug 2025 17:27:08 -0600 Subject: [PATCH 28/34] bug fixes: Enhance AWS SSO profile creation with nil checks for required fields and improve error handling --- cmd/cone/get_drop_task.go | 43 +++++++++++++++++++++++++++++++---- cmd/cone/task_approve_deny.go | 28 +++++++++++++++++++---- pkg/client/client.go | 2 +- pkg/client/task.go | 8 +++++-- 4 files changed, 70 insertions(+), 11 deletions(-) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index 12f8945b..5b524f05 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -288,8 +288,14 @@ func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *s return nil } - appID := *task.TaskType.TaskTypeGrant.AppID - entitlementID := *task.TaskType.TaskTypeGrant.AppEntitlementID + grantTask := task.TaskType.TaskTypeGrant + if grantTask.AppID == nil || grantTask.AppEntitlementID == nil { + // Skip if required fields are missing - not an error for this function + return nil + } + + appID := *grantTask.AppID + entitlementID := *grantTask.AppEntitlementID // Get the entitlement details entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) @@ -297,6 +303,12 @@ func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *s return fmt.Errorf("failed to get entitlement details: %w", err) } + // Check for nil pointers before dereferencing + if entitlement.AppResourceTypeID == nil { + // Not an error condition, just skip AWS SSO profile creation + return nil + } + // Get the resource type resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) if err != nil { @@ -305,6 +317,10 @@ func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *s // Check if this is an AWS permission set if client.IsAWSPermissionSet(entitlement, resourceType) { + if entitlement.AppResourceID == nil { + return fmt.Errorf("entitlement AppResourceID is nil, cannot create AWS SSO profile") + } + // Get the resource details resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) if err != nil { @@ -320,15 +336,36 @@ func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *s // handleWaitBehavior manages the waiting state for task completion. func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { + // Validate input parameters + if task == nil || task.ID == nil { + return fmt.Errorf("task or task ID is nil") + } + spinner, _ := pterm.DefaultSpinner.Start("Waiting for task to complete...") defer spinner.Stop() for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(2 * time.Second): + // Continue with polling + } + updatedTask, err := c.GetTask(ctx, *task.ID) if err != nil { return err } + // Check for nil pointers before dereferencing + if updatedTask.TaskView == nil || updatedTask.TaskView.Task == nil { + return fmt.Errorf("received incomplete task response") + } + + if updatedTask.TaskView.Task.State == nil { + return fmt.Errorf("task state is nil") + } + if *updatedTask.TaskView.Task.State == shared.TaskStateTaskStateClosed { if updatedTask.TaskView.Task.TaskType.TaskTypeGrant != nil { taskOutcome := updatedTask.TaskView.Task.TaskType.TaskTypeGrant.Outcome @@ -348,8 +385,6 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas } break } - - time.Sleep(2 * time.Second) } return nil diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index eb3a0174..d5627ecb 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -51,12 +51,20 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { switch { case taskResp.TaskView.Task.TaskType.TaskTypeGrant != nil: // Handle grant task - appID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppID - entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeGrant.AppEntitlementID + grantTask := taskResp.TaskView.Task.TaskType.TaskTypeGrant + if grantTask.AppID == nil || grantTask.AppEntitlementID == nil { + return nil, fmt.Errorf("grant task is missing required AppID or AppEntitlementID") + } + appID = *grantTask.AppID + entitlementID = *grantTask.AppEntitlementID case taskResp.TaskView.Task.TaskType.TaskTypeRevoke != nil: // Handle revoke task - appID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppID - entitlementID = *taskResp.TaskView.Task.TaskType.TaskTypeRevoke.AppEntitlementID + revokeTask := taskResp.TaskView.Task.TaskType.TaskTypeRevoke + if revokeTask.AppID == nil || revokeTask.AppEntitlementID == nil { + return nil, fmt.Errorf("revoke task is missing required AppID or AppEntitlementID") + } + appID = *revokeTask.AppID + entitlementID = *revokeTask.AppEntitlementID default: return nil, fmt.Errorf("unsupported task type") } @@ -78,6 +86,12 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { } pterm.Success.Println("Task approved successfully") + // Check for nil pointers before dereferencing + if entitlement.AppResourceTypeID == nil { + pterm.Warning.Println("Entitlement AppResourceTypeID is nil, skipping AWS SSO profile creation") + return approveResp.TaskView.Task, nil + } + resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) if err != nil { pterm.Warning.Printf("Failed to get resource type details: %v\n", err) @@ -87,6 +101,12 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { if client.IsAWSPermissionSet(entitlement, resourceType) { pterm.Info.Println("Detected AWS permission set, getting resource details...") + + if entitlement.AppResourceID == nil { + pterm.Warning.Println("Entitlement AppResourceID is nil, cannot create AWS SSO profile") + return approveResp.TaskView.Task, nil + } + resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) if err != nil { pterm.Warning.Printf("Failed to get resource details: %v\n", err) diff --git a/pkg/client/client.go b/pkg/client/client.go index 421ea90b..9f2ac820 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -63,7 +63,7 @@ type C1Client interface { GetUser(ctx context.Context, userID string) (*shared.User, error) GetEntitlement(ctx context.Context, appID string, entitlementID string) (*shared.AppEntitlement, error) SearchEntitlements(ctx context.Context, filter *SearchEntitlementsFilter) ([]*EntitlementWithBindings, error) - GetResource(ctx context.Context, appID string, resourceID string, resourceTypeID string) (*shared.AppResource, error) + GetResource(ctx context.Context, appID string, resourceTypeID string, resourceID string) (*shared.AppResource, error) GetResourceType(ctx context.Context, appID string, resourceTypeID string) (*shared.AppResourceType, error) GetApp(ctx context.Context, appID string) (*shared.App, error) GetTask(ctx context.Context, taskId string) (*shared.TaskServiceGetResponse, error) diff --git a/pkg/client/task.go b/pkg/client/task.go index 34d1a118..32f172b4 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -223,7 +223,11 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap } // Extract role name from entitlement display name (everything before first space) - roleName := strings.Split(*entitlement.DisplayName, " ")[0] + if entitlement.DisplayName == nil { + return errors.New("entitlement DisplayName is nil, cannot create AWS SSO profile") + } + displayName := *entitlement.DisplayName // Store safely after nil check + roleName := strings.Split(displayName, " ")[0] // Get AWS account name from resource display name accountName := "aws" @@ -292,6 +296,6 @@ sso_registration_scopes = sso:account:access return fmt.Errorf("failed to write AWS config file: %w", err) } - pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", *entitlement.DisplayName) + pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", displayName) return nil } From 4d7f59eac9b213da95cd145924a20bf575274842 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Mon, 11 Aug 2025 18:06:53 -0600 Subject: [PATCH 29/34] linting issues --- .golangci.yml | 6 +++--- cmd/cone/config.go | 7 ++++--- cmd/cone/get_drop_task.go | 39 ++++++----------------------------- cmd/cone/task_approve_deny.go | 4 ++-- pkg/client/task.go | 6 ++++-- 5 files changed, 19 insertions(+), 43 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 4ab47e29..0bd596e6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,8 +47,8 @@ linters-settings: - name: var-naming arguments: [["ID", "URL", "HTTP", "API"], []] - tenv: - all: true + usetesting: + allow: [] varcheck: exported-fields: false # this appears to improperly detect exported variables as unused when they are used from a package with the same name @@ -88,7 +88,7 @@ linters: - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. - predeclared # find code that shadows one of Go's predeclared identifiers - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. - - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 + - usetesting # usetesting is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # Remove unnecessary type conversions - usestdlibvars # detect the possibility to use variables/constants from the Go standard library diff --git a/cmd/cone/config.go b/cmd/cone/config.go index ca032fca..d5b3fc31 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -13,7 +13,8 @@ import ( ) const ( - envPrefix = "cone" + envPrefix = "cone" + nativeIntegrationMode = "native" ) func defaultConfigPath() string { @@ -118,7 +119,7 @@ func showAWSConfigCmd() *cobra.Command { if err != nil { return fmt.Errorf("error reading config file: %w", err) } - fmt.Println(string(content)) + pterm.Println(string(content)) return nil } @@ -133,7 +134,7 @@ func showAWSConfigCmd() *cobra.Command { // Get integration mode integrationMode := viper.GetString("aws_integration_mode") if integrationMode == "" { - integrationMode = "native" // Default to native if not set + integrationMode = nativeIntegrationMode // Default to native if not set } pterm.Info.Printf("AWS integration mode: %s\n", integrationMode) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index 5b524f05..43b60776 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -254,34 +254,6 @@ func runDrop(cmd *cobra.Command, args []string) error { }) } -func printExtraTaskDetails(ctx context.Context, v *viper.Viper, c client.C1Client, appId string, entitlementId string) error { - outputManager := output.NewManager(ctx, v) - - appVal, err := c.GetApp(ctx, appId) - if err != nil { - return err - } - - entitlementVal, err := c.GetEntitlement(ctx, appId, entitlementId) - if err != nil { - return err - } - - app := App{app: appVal, client: c} - err = outputManager.Output(ctx, &app, output.WithTransposeTable()) - if err != nil { - return err - } - - entitlement := Entitlement{entitlement: entitlementVal, client: c} - err = outputManager.Output(ctx, &entitlement, output.WithTransposeTable()) - if err != nil { - return err - } - - return nil -} - // createAWSSSOProfileIfNeeded checks if the entitlement is an AWS permission set and creates the profile if needed. func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *shared.Task, outputManager output.Manager) error { if task.TaskType.TaskTypeGrant == nil { @@ -320,7 +292,7 @@ func createAWSSSOProfileIfNeeded(ctx context.Context, c client.C1Client, task *s if entitlement.AppResourceID == nil { return fmt.Errorf("entitlement AppResourceID is nil, cannot create AWS SSO profile") } - + // Get the resource details resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) if err != nil { @@ -342,7 +314,7 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas } spinner, _ := pterm.DefaultSpinner.Start("Waiting for task to complete...") - defer spinner.Stop() + defer func() { _ = spinner.Stop() }() for { select { @@ -373,12 +345,13 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas return fmt.Errorf("task closed but no outcome provided") } - if *taskOutcome == shared.TaskTypeGrantOutcomeGrantOutcomeGranted { + switch *taskOutcome { + case shared.TaskTypeGrantOutcomeGrantOutcomeGranted: spinner.Success("Entitlement granted successfully.") - } else if *taskOutcome == shared.TaskTypeGrantOutcomeGrantOutcomeDenied { + case shared.TaskTypeGrantOutcomeGrantOutcomeDenied: spinner.Fail("Entitlement request was denied.") return fmt.Errorf("entitlement request was denied") - } else { + default: spinner.Fail(fmt.Sprintf("Task completed with unexpected outcome: %s", *taskOutcome)) return fmt.Errorf("task completed with unexpected outcome: %s", *taskOutcome) } diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index d5627ecb..3e1f8df0 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -101,12 +101,12 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { if client.IsAWSPermissionSet(entitlement, resourceType) { pterm.Info.Println("Detected AWS permission set, getting resource details...") - + if entitlement.AppResourceID == nil { pterm.Warning.Println("Entitlement AppResourceID is nil, cannot create AWS SSO profile") return approveResp.TaskView.Task, nil } - + resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) if err != nil { pterm.Warning.Printf("Failed to get resource details: %v\n", err) diff --git a/pkg/client/task.go b/pkg/client/task.go index 32f172b4..6b9794e6 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -14,6 +14,8 @@ import ( "github.com/spf13/viper" ) +const nativeIntegrationMode = "native" + func (c *client) GetTask(ctx context.Context, taskId string) (*shared.TaskServiceGetResponse, error) { resp, err := c.sdk.Task.Get(ctx, operations.C1APITaskV1TaskServiceGetRequest{ID: taskId}) if err != nil { @@ -199,10 +201,10 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap // Check integration mode integrationMode := viper.GetString("aws_integration_mode") if integrationMode == "" { - integrationMode = "native" // Default to native if not set + integrationMode = nativeIntegrationMode // Default to native if not set } - if integrationMode == "native" { + if integrationMode == nativeIntegrationMode { // In native mode, we don't create profiles return nil } From f6f72e3c890db2652fdf20e9e8071b8867e96584 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 12 Aug 2025 08:36:43 -0600 Subject: [PATCH 30/34] cursor bot bug suggestions fixes --- cmd/cone/aws_credentials.go | 12 +++++++++++- cmd/cone/get_drop_task.go | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cmd/cone/aws_credentials.go b/cmd/cone/aws_credentials.go index be80e832..847997db 100644 --- a/cmd/cone/aws_credentials.go +++ b/cmd/cone/aws_credentials.go @@ -296,7 +296,17 @@ func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error // checkC1Access verifies if the user has access to the requested AWS profile. // by checking their grants in ConductorOne. func checkC1Access(ctx context.Context, profileName string) (bool, error) { - cmd := &cobra.Command{} + // Create a temporary command with the necessary flags for cmdContext + cmd := &cobra.Command{ + Use: "temp", + } + cmd.PersistentFlags().StringP("profile", "p", "default", "The config profile to use.") + cmd.PersistentFlags().BoolP("non-interactive", "i", false, "Disable prompts.") + cmd.PersistentFlags().String("client-id", "", "Client ID") + cmd.PersistentFlags().String("client-secret", "", "Client secret") + cmd.PersistentFlags().String("api-endpoint", "", "Override the API endpoint") + cmd.PersistentFlags().StringP("output", "o", "table", "Output format. Valid values: table, json, json-pretty, wide.") + cmd.PersistentFlags().Bool("debug", false, "Enable debug logging") cmd.SetContext(ctx) _, c1Client, _, err := cmdContext(cmd) if err != nil { diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index 43b60776..b8ce571b 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -355,6 +355,22 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas spinner.Fail(fmt.Sprintf("Task completed with unexpected outcome: %s", *taskOutcome)) return fmt.Errorf("task completed with unexpected outcome: %s", *taskOutcome) } + } else if updatedTask.TaskView.Task.TaskType.TaskTypeRevoke != nil { + taskOutcome := updatedTask.TaskView.Task.TaskType.TaskTypeRevoke.Outcome + if taskOutcome == nil { + return fmt.Errorf("task closed but no outcome provided") + } + + switch *taskOutcome { + case shared.TaskTypeRevokeOutcomeRevokeOutcomeRevoked: + spinner.Success("Entitlement revoked successfully.") + case shared.TaskTypeRevokeOutcomeRevokeOutcomeDenied: + spinner.Fail("Entitlement revoke request was denied.") + return fmt.Errorf("entitlement revoke request was denied") + default: + spinner.Fail(fmt.Sprintf("Task completed with unexpected outcome: %s", *taskOutcome)) + return fmt.Errorf("task completed with unexpected outcome: %s", *taskOutcome) + } } break } From 39114b1d020e432f3d20a2f086de85ae3fd44d32 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 12 Aug 2025 11:22:41 -0600 Subject: [PATCH 31/34] cursor both bug fixes: Refactor AWS SSO and integration mode configuration to use SafeWriteConfig; enhance admin permission verification and add detailed entitlement info display --- cmd/cone/config.go | 4 +-- cmd/cone/generate_alias.go | 18 +++++++++-- cmd/cone/get_drop_task.go | 66 +++++++++++++++++++++++++++++++++++++- pkg/client/entitlement.go | 10 ++++-- 4 files changed, 91 insertions(+), 7 deletions(-) diff --git a/cmd/cone/config.go b/cmd/cone/config.go index d5b3fc31..f7b2bdf5 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -182,7 +182,7 @@ func setAWSSSOStartURLCmd() *cobra.Command { } url := args[0] viper.Set("aws_sso_start_url", url) - if err := viper.WriteConfig(); err != nil { + if err := viper.SafeWriteConfig(); err != nil { return err } pterm.Info.Printf("AWS SSO start URL set to: %s\n", url) @@ -224,7 +224,7 @@ func setAWSIntegrationModeCmd() *cobra.Command { return fmt.Errorf("mode must be either 'cone' or 'native'") } viper.Set("aws_integration_mode", mode) - if err := viper.WriteConfig(); err != nil { + if err := viper.SafeWriteConfig(); err != nil { return err } pterm.Info.Printf("AWS integration mode set to: %s\n", mode) diff --git a/cmd/cone/generate_alias.go b/cmd/cone/generate_alias.go index 2ba9dda4..84b75054 100644 --- a/cmd/cone/generate_alias.go +++ b/cmd/cone/generate_alias.go @@ -120,7 +120,7 @@ func generateAliasRun(cmd *cobra.Command, args []string) error { } // Verify admin permissions - if err := verifyAdminPermissions(ctx, c); err != nil { + if err := verifyAdminPermissions(ctx, c, v); err != nil { return err } @@ -329,7 +329,21 @@ func processEntitlement(ctx context.Context, c client.C1Client, e *client.Entitl } // verifyAdminPermissions checks if the user has admin permissions. -func verifyAdminPermissions(ctx context.Context, c client.C1Client) error { +func verifyAdminPermissions(ctx context.Context, c client.C1Client, v *viper.Viper) error { + // In non-interactive mode, skip user prompt and check permissions directly + if v.GetBool(nonInteractiveFlag) { + // Just check actual permissions without prompting + isAdmin, err := checkAdminPermissions(ctx, c) + if err != nil { + return fmt.Errorf("failed to check admin permissions: %w", err) + } + if !isAdmin { + return fmt.Errorf("you do not have super admin or app admin permissions. Use --help for more information") + } + return nil + } + + // Interactive mode: prompt user first, then check permissions // Prompt the user pterm.Info.Print("Are you a super admin or app admin? (yes/no): ") reader := bufio.NewReader(os.Stdin) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index b8ce571b..c99f1484 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -408,6 +408,15 @@ func runTask( } outputManager := output.NewManager(ctx, v) + + // Show detailed app and entitlement information if requested + if v.GetBool(extraDetailsFlag) { + err = showDetailedEntitlementInfo(ctx, c, appId, entitlementId) + if err != nil { + pterm.Warning.Printf("Failed to show detailed entitlement information: %v\n", err) + } + } + taskResp := Task{task: task, client: c} err = outputManager.Output(ctx, &taskResp, output.WithTransposeTable()) if err != nil { @@ -416,7 +425,7 @@ func runTask( // Create AWS SSO profile immediately after task creation if err := createAWSSSOProfileIfNeeded(ctx, c, task, outputManager); err != nil { - return err + pterm.Warning.Printf("Failed to create AWS SSO profile: %v\n", err) } if wait, _ := cmd.Flags().GetBool("wait"); wait { @@ -593,3 +602,58 @@ func multipleEntitlmentsFoundError(alias string, query string) error { } return fmt.Errorf("multiple entitlements found, please specify an entitlement id and app id") } + +// showDetailedEntitlementInfo displays detailed information about the app and entitlement. +func showDetailedEntitlementInfo(ctx context.Context, c client.C1Client, appID string, entitlementID string) error { + pterm.DefaultSection.Println("Entitlement Details") + + // Get app details + app, err := c.GetApp(ctx, appID) + if err != nil { + return fmt.Errorf("failed to get app details: %w", err) + } + + // Get entitlement details + entitlement, err := c.GetEntitlement(ctx, appID, entitlementID) + if err != nil { + return fmt.Errorf("failed to get entitlement details: %w", err) + } + + // Display app information + pterm.DefaultBasicText.Printf("App: %s\n", client.StringFromPtr(app.DisplayName)) + if app.Description != nil && *app.Description != "" { + pterm.DefaultBasicText.Printf("App Description: %s\n", client.StringFromPtr(app.Description)) + } + + // Display entitlement information + pterm.DefaultBasicText.Printf("Entitlement: %s\n", client.StringFromPtr(entitlement.DisplayName)) + if entitlement.Description != nil && *entitlement.Description != "" { + pterm.DefaultBasicText.Printf("Entitlement Description: %s\n", client.StringFromPtr(entitlement.Description)) + } + + // Show resource type and resource information if available + if entitlement.AppResourceTypeID != nil { + resourceType, err := c.GetResourceType(ctx, appID, *entitlement.AppResourceTypeID) + if err == nil { + pterm.DefaultBasicText.Printf("Resource Type: %s\n", client.StringFromPtr(resourceType.DisplayName)) + + if entitlement.AppResourceID != nil { + resource, err := c.GetResource(ctx, appID, *entitlement.AppResourceTypeID, *entitlement.AppResourceID) + if err == nil { + pterm.DefaultBasicText.Printf("Resource: %s\n", client.StringFromPtr(resource.DisplayName)) + if resource.Description != nil && *resource.Description != "" { + pterm.DefaultBasicText.Printf("Resource Description: %s\n", client.StringFromPtr(resource.Description)) + } + } + } + } + } + + // Show duration information if available + if entitlement.DurationGrant != nil && *entitlement.DurationGrant != "" { + pterm.DefaultBasicText.Printf("Max Grant Duration: %s\n", *entitlement.DurationGrant) + } + + pterm.Println() // Add spacing before task output + return nil +} diff --git a/pkg/client/entitlement.go b/pkg/client/entitlement.go index 38bbb3cd..ebb91148 100644 --- a/pkg/client/entitlement.go +++ b/pkg/client/entitlement.go @@ -223,10 +223,16 @@ func (c *client) ListEntitlements(ctx context.Context, appId string) ([]shared.A } func (c *client) UpdateEntitlement(ctx context.Context, appID, entitlementID string, req *shared.UpdateAppEntitlementRequest) error { - _, err := c.sdk.AppEntitlements.Update(ctx, operations.C1APIAppV1AppEntitlementsUpdateRequest{ + resp, err := c.sdk.AppEntitlements.Update(ctx, operations.C1APIAppV1AppEntitlementsUpdateRequest{ AppID: appID, ID: entitlementID, UpdateAppEntitlementRequest: req, }) - return err + if err != nil { + return err + } + if err := NewHTTPError(resp.RawResponse); err != nil { + return err + } + return nil } From c9f1b175117450286d3061537f8c22a5548006e0 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 12 Aug 2025 12:38:12 -0600 Subject: [PATCH 32/34] more cursor fixes :( --- cmd/cone/aws_credentials.go | 6 +++--- cmd/cone/config.go | 4 ++-- cmd/cone/get_drop_task.go | 10 ++++++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/cone/aws_credentials.go b/cmd/cone/aws_credentials.go index 847997db..f9e4033c 100644 --- a/cmd/cone/aws_credentials.go +++ b/cmd/cone/aws_credentials.go @@ -101,7 +101,7 @@ func awsCredentialsRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("SSO session verification failed: %w", err) } - creds, err := getTemporaryCredentials(accountID, roleName) + creds, err := getTemporaryCredentials(accountID, roleName, ssoRegion) if err != nil { return fmt.Errorf("failed to get temporary credentials: %w", err) } @@ -238,7 +238,7 @@ func getSSOToken(ssoStartURL string) (string, error) { // getTemporaryCredentials retrieves temporary AWS credentials using AWS SSO. // It handles the SSO login process if needed and returns the credentials. -func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error) { +func getTemporaryCredentials(accountID, roleName, ssoRegion string) (*AWSCredentials, error) { ssoStartURL := viper.GetString("aws_sso_start_url") if ssoStartURL == "" { return nil, fmt.Errorf("missing AWS SSO URL. Please run 'cone config-aws set-sso-url ' first") @@ -260,7 +260,7 @@ func getTemporaryCredentials(accountID, roleName string) (*AWSCredentials, error "--access-token", token, "--account-id", accountID, "--role-name", roleName, - "--region", "us-east-1", + "--region", ssoRegion, "--output", "json") var stdout, stderr bytes.Buffer diff --git a/cmd/cone/config.go b/cmd/cone/config.go index f7b2bdf5..d5b3fc31 100644 --- a/cmd/cone/config.go +++ b/cmd/cone/config.go @@ -182,7 +182,7 @@ func setAWSSSOStartURLCmd() *cobra.Command { } url := args[0] viper.Set("aws_sso_start_url", url) - if err := viper.SafeWriteConfig(); err != nil { + if err := viper.WriteConfig(); err != nil { return err } pterm.Info.Printf("AWS SSO start URL set to: %s\n", url) @@ -224,7 +224,7 @@ func setAWSIntegrationModeCmd() *cobra.Command { return fmt.Errorf("mode must be either 'cone' or 'native'") } viper.Set("aws_integration_mode", mode) - if err := viper.SafeWriteConfig(); err != nil { + if err := viper.WriteConfig(); err != nil { return err } pterm.Info.Printf("AWS integration mode set to: %s\n", mode) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index c99f1484..ba062440 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -411,7 +411,7 @@ func runTask( // Show detailed app and entitlement information if requested if v.GetBool(extraDetailsFlag) { - err = showDetailedEntitlementInfo(ctx, c, appId, entitlementId) + err = showDetailedEntitlementInfo(ctx, c, appId, entitlementId, v) if err != nil { pterm.Warning.Printf("Failed to show detailed entitlement information: %v\n", err) } @@ -604,7 +604,13 @@ func multipleEntitlmentsFoundError(alias string, query string) error { } // showDetailedEntitlementInfo displays detailed information about the app and entitlement. -func showDetailedEntitlementInfo(ctx context.Context, c client.C1Client, appID string, entitlementID string) error { +func showDetailedEntitlementInfo(ctx context.Context, c client.C1Client, appID string, entitlementID string, v *viper.Viper) error { + // Skip detailed output for JSON formats to avoid mixing plain text with structured output + outputFormat := v.GetString("output") + if outputFormat == "json" || outputFormat == "json-pretty" { + return nil + } + pterm.DefaultSection.Println("Entitlement Details") // Get app details From 7e9148c66340ed52267ff69366ec56056e2942e9 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 12 Aug 2025 13:42:35 -0600 Subject: [PATCH 33/34] cursor fixes: Enhance AWS SSO profile handling: Move profile creation to after successful grant, add output format checks for logging --- cmd/cone/generate_alias.go | 2 +- cmd/cone/get_drop_task.go | 17 +++++++++++++---- cmd/cone/task_approve_deny.go | 20 ++++++++++++++------ pkg/client/task.go | 12 ++++++++++-- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/cmd/cone/generate_alias.go b/cmd/cone/generate_alias.go index 84b75054..ad51719c 100644 --- a/cmd/cone/generate_alias.go +++ b/cmd/cone/generate_alias.go @@ -508,7 +508,7 @@ func checkAdminPermissions(ctx context.Context, c client.C1Client) (bool, error) // Check if user is an admin for this app for _, appUser := range appUsers { - if appUser.IdentityUserID != nil && *appUser.IdentityUserID == *userIntro.UserID { + if appUser.IdentityUserID != nil && userIntro.UserID != nil && *appUser.IdentityUserID == *userIntro.UserID { // Check if user has admin role in their profile if profile, ok := appUser.Profile["roles"]; ok { if roles, ok := profile.([]interface{}); ok { diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index ba062440..8cfd802b 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -348,6 +348,10 @@ func handleWaitBehavior(ctx context.Context, c client.C1Client, task *shared.Tas switch *taskOutcome { case shared.TaskTypeGrantOutcomeGrantOutcomeGranted: spinner.Success("Entitlement granted successfully.") + // Create AWS SSO profile now that grant is successful + if err := createAWSSSOProfileIfNeeded(ctx, c, updatedTask.TaskView.Task, outputManager); err != nil { + pterm.Warning.Printf("Failed to create AWS SSO profile: %v\n", err) + } case shared.TaskTypeGrantOutcomeGrantOutcomeDenied: spinner.Fail("Entitlement request was denied.") return fmt.Errorf("entitlement request was denied") @@ -404,6 +408,14 @@ func runTask( task, err := run(c, ctx, appId, entitlementId, client.StringFromPtr(resp.UserID), justification) if err != nil { + // Check if this is a duplicate grant error and force flag is not set + errorMsg := err.Error() + force := v.GetBool(forceFlag) + if !force && (strings.Contains(strings.ToLower(errorMsg), "already granted") || + strings.Contains(strings.ToLower(errorMsg), "already exists") || + strings.Contains(strings.ToLower(errorMsg), "duplicate")) { + return fmt.Errorf("%s. Use --force flag to override this check", err.Error()) + } return err } @@ -423,10 +435,7 @@ func runTask( return err } - // Create AWS SSO profile immediately after task creation - if err := createAWSSSOProfileIfNeeded(ctx, c, task, outputManager); err != nil { - pterm.Warning.Printf("Failed to create AWS SSO profile: %v\n", err) - } + // Note: AWS SSO profile creation moved to handleWaitBehavior to occur only after successful grant completion if wait, _ := cmd.Flags().GetBool("wait"); wait { err = handleWaitBehavior(ctx, c, task, outputManager) diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index 3e1f8df0..d74567c5 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -7,6 +7,7 @@ import ( "github.com/pterm/pterm" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" "github.com/conductorone/cone/pkg/client" @@ -38,14 +39,21 @@ func denyTasksCmd() *cobra.Command { } func runApproveTasks(cmd *cobra.Command, args []string) error { - return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { - pterm.Info.Printf("Starting task approval process for task %s\n", taskId) + return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string, v *viper.Viper) (*shared.Task, error) { + // Only show info message for non-JSON output to avoid corrupting structured output + outputFormat := v.GetString("output") + if outputFormat != "json" && outputFormat != "json-pretty" { + pterm.Info.Printf("Starting task approval process for task %s\n", taskId) + } taskResp, err := c.GetTask(ctx, taskId) if err != nil { return nil, err } - pterm.Debug.Printf("Got task details: %+v\n", taskResp.TaskView.Task) + // Only show debug message for non-JSON output to avoid corrupting structured output + if outputFormat != "json" && outputFormat != "json-pretty" { + pterm.Debug.Printf("Got task details: %+v\n", taskResp.TaskView.Task) + } var appID, entitlementID string switch { @@ -127,7 +135,7 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { } func runDenyTasks(cmd *cobra.Command, args []string) error { - return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error) { + return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string, v *viper.Viper) (*shared.Task, error) { approveResp, err := c.DenyTask(ctx, taskId, comment, policyId) if err != nil { return nil, err @@ -139,7 +147,7 @@ func runDenyTasks(cmd *cobra.Command, args []string) error { func runApproveDeny( cmd *cobra.Command, args []string, - run func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string) (*shared.Task, error), + run func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string, v *viper.Viper) (*shared.Task, error), ) error { ctx, c, v, err := cmdContext(cmd) if err != nil { @@ -162,7 +170,7 @@ func runApproveDeny( return errors.New("task does not have a current policy step id and cannot be approved or denied") } - task, err := run(c, ctx, taskId, comment, client.StringFromPtr(taskResp.TaskView.Task.PolicyInstance.PolicyStepInstance.ID)) + task, err := run(c, ctx, taskId, comment, client.StringFromPtr(taskResp.TaskView.Task.PolicyInstance.PolicyStepInstance.ID), v) if err != nil { return err } diff --git a/pkg/client/task.go b/pkg/client/task.go index 6b9794e6..6d7a54f0 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -262,7 +262,11 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap // Check if profile already exists configStr := string(configContent) if strings.Contains(configStr, fmt.Sprintf("[profile %s]", profileName)) { - pterm.Info.Printf("AWS profile '%s' already exists\n", profileName) + // Only show info message for non-JSON output to avoid corrupting structured output + outputFormat := viper.GetString("output") + if outputFormat != "json" && outputFormat != "json-pretty" { + pterm.Info.Printf("AWS profile '%s' already exists\n", profileName) + } return nil } @@ -298,6 +302,10 @@ sso_registration_scopes = sso:account:access return fmt.Errorf("failed to write AWS config file: %w", err) } - pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", displayName) + // Only show success message for non-JSON output to avoid corrupting structured output + outputFormat := viper.GetString("output") + if outputFormat != "json" && outputFormat != "json-pretty" { + pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", displayName) + } return nil } From 87ff60dea4d195a53470844d0bf9261b6fff3249 Mon Sep 17 00:00:00 2001 From: Ali Falahi Date: Tue, 12 Aug 2025 14:21:01 -0600 Subject: [PATCH 34/34] refactor: Update output format checks to use constants for JSON handling --- cmd/cone/get_drop_task.go | 4 ++-- cmd/cone/task_approve_deny.go | 4 ++-- pkg/client/error.go | 2 +- pkg/client/task.go | 6 ++++-- pkg/output/output.go | 7 +++++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd/cone/get_drop_task.go b/cmd/cone/get_drop_task.go index 8cfd802b..974da42c 100644 --- a/cmd/cone/get_drop_task.go +++ b/cmd/cone/get_drop_task.go @@ -411,7 +411,7 @@ func runTask( // Check if this is a duplicate grant error and force flag is not set errorMsg := err.Error() force := v.GetBool(forceFlag) - if !force && (strings.Contains(strings.ToLower(errorMsg), "already granted") || + if !force && (strings.Contains(strings.ToLower(errorMsg), "already granted") || strings.Contains(strings.ToLower(errorMsg), "already exists") || strings.Contains(strings.ToLower(errorMsg), "duplicate")) { return fmt.Errorf("%s. Use --force flag to override this check", err.Error()) @@ -616,7 +616,7 @@ func multipleEntitlmentsFoundError(alias string, query string) error { func showDetailedEntitlementInfo(ctx context.Context, c client.C1Client, appID string, entitlementID string, v *viper.Viper) error { // Skip detailed output for JSON formats to avoid mixing plain text with structured output outputFormat := v.GetString("output") - if outputFormat == "json" || outputFormat == "json-pretty" { + if outputFormat == output.JSON || outputFormat == output.JSONPretty { return nil } diff --git a/cmd/cone/task_approve_deny.go b/cmd/cone/task_approve_deny.go index d74567c5..c43e5f67 100644 --- a/cmd/cone/task_approve_deny.go +++ b/cmd/cone/task_approve_deny.go @@ -42,7 +42,7 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { return runApproveDeny(cmd, args, func(c client.C1Client, ctx context.Context, taskId string, comment string, policyId string, v *viper.Viper) (*shared.Task, error) { // Only show info message for non-JSON output to avoid corrupting structured output outputFormat := v.GetString("output") - if outputFormat != "json" && outputFormat != "json-pretty" { + if outputFormat != output.JSON && outputFormat != output.JSONPretty { pterm.Info.Printf("Starting task approval process for task %s\n", taskId) } @@ -51,7 +51,7 @@ func runApproveTasks(cmd *cobra.Command, args []string) error { return nil, err } // Only show debug message for non-JSON output to avoid corrupting structured output - if outputFormat != "json" && outputFormat != "json-pretty" { + if outputFormat != output.JSON && outputFormat != output.JSONPretty { pterm.Debug.Printf("Got task details: %+v\n", taskResp.TaskView.Task) } diff --git a/pkg/client/error.go b/pkg/client/error.go index c597f480..7152e97f 100644 --- a/pkg/client/error.go +++ b/pkg/client/error.go @@ -55,7 +55,7 @@ func HandleErrors(ctx context.Context, v *viper.Viper, input error) error { return input } outputType := v.GetString("output") - if outputType != "json" && outputType != output.JSONPretty { + if outputType != output.JSON && outputType != output.JSONPretty { return input } var jsonError []byte diff --git a/pkg/client/task.go b/pkg/client/task.go index 6d7a54f0..81d1682b 100644 --- a/pkg/client/task.go +++ b/pkg/client/task.go @@ -12,6 +12,8 @@ import ( "github.com/conductorone/conductorone-sdk-go/pkg/models/shared" "github.com/pterm/pterm" "github.com/spf13/viper" + + "github.com/conductorone/cone/pkg/output" ) const nativeIntegrationMode = "native" @@ -264,7 +266,7 @@ func CreateAWSSSOProfile(entitlement *shared.AppEntitlement, resource *shared.Ap if strings.Contains(configStr, fmt.Sprintf("[profile %s]", profileName)) { // Only show info message for non-JSON output to avoid corrupting structured output outputFormat := viper.GetString("output") - if outputFormat != "json" && outputFormat != "json-pretty" { + if outputFormat != output.JSON && outputFormat != output.JSONPretty { pterm.Info.Printf("AWS profile '%s' already exists\n", profileName) } return nil @@ -304,7 +306,7 @@ sso_registration_scopes = sso:account:access // Only show success message for non-JSON output to avoid corrupting structured output outputFormat := viper.GetString("output") - if outputFormat != "json" && outputFormat != "json-pretty" { + if outputFormat != output.JSON && outputFormat != output.JSONPretty { pterm.Success.Printf("Successfully created AWS SSO profile for entitlement %s\n", displayName) } return nil diff --git a/pkg/output/output.go b/pkg/output/output.go index 4ff4758b..334680a2 100644 --- a/pkg/output/output.go +++ b/pkg/output/output.go @@ -11,7 +11,10 @@ type Manager interface { Output(ctx context.Context, out interface{}, opts ...outputOption) error } -const JSONPretty = "json-pretty" +const ( + JSON = "json" + JSONPretty = "json-pretty" +) func NewManager(ctx context.Context, v *viper.Viper) Manager { var area *pterm.AreaPrinter @@ -22,7 +25,7 @@ func NewManager(ctx context.Context, v *viper.Viper) Manager { switch v.GetString("output") { case "table": return &tableManager{area: area, isWide: false} - case "json": + case JSON: return &jsonManager{} case JSONPretty: return &jsonManager{pretty: true}