-
Notifications
You must be signed in to change notification settings - Fork 3
Ali/cone improvements #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
afalahi
wants to merge
34
commits into
main
Choose a base branch
from
ali/cone-improvements
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
209270d
Add AWS credentials command to retrieve temporary credentials using A…
afalahi 4584254
Add root command for Cone CLI with multiple subcommands
afalahi 8767854
Implement AWS SSO configuration commands in Cone CLI
afalahi d47a1da
Add alias generation command to Cone CLI
afalahi c1a8472
Add AWS SSO profile creation in task completion handling
afalahi 97283b5
Refactor CLI command initialization in Cone
afalahi 5800106
Enhance search entitlements command in Cone CLI
afalahi 431b308
Enhance task approval process in Cone CLI
afalahi 0e55d9b
Enhance task search command in Cone CLI
afalahi 80570b3
Enhance token command in Cone CLI
afalahi 7933985
Add UpdateEntitlement method to C1Client interface
afalahi 6bca30e
Add UpdateEntitlement method to client
afalahi 14a6cc0
Add AWS SSO profile creation and permission set validation
afalahi 2a854d9
Update warning messages for consistency in get_drop_task.go
afalahi 4455830
Refactor AWS credentials structure and improve documentation
afalahi 5dd0831
Enhance AWS SSO command functionality and improve user feedback
afalahi 6101bc6
Refactor comments and enhance user feedback in alias generation
afalahi ba504e2
Refactor error handling and improve documentation in get_drop_task.go
afalahi 2c191c1
Refactor task approval process to enhance user feedback and improve l…
afalahi 1b32319
Improve documentation comments in task.go for clarity and consistency
afalahi c4e4c81
Remove unused task command registrations from the Cone CLI root comma…
afalahi a76db56
Add AWS SSO profile creation and task wait handling
afalahi f4a55cf
Enhance AWS SSO profile creation feedback with styled output
afalahi 1a20f16
Add AWS configuration commands for integration mode and display settings
afalahi bc5ebfd
Add integration mode check in CreateAWSSSOProfile function
afalahi 7254b80
Add raw flag to show AWS configuration command and update default int…
afalahi 4f55bd0
Update default AWS integration mode to 'native' in CreateAWSSSOProfil…
afalahi e883311
bug fixes: Enhance AWS SSO profile creation with nil checks for requi…
afalahi 4d7f59e
linting issues
afalahi f6f72e3
cursor bot bug suggestions fixes
afalahi 39114b1
cursor both bug fixes: Refactor AWS SSO and integration mode configur…
afalahi c9f1b17
more cursor fixes :(
afalahi 7e9148c
cursor fixes: Enhance AWS SSO profile handling: Move profile creation…
afalahi 87ff60d
refactor: Update output format checks to use constants for JSON handling
afalahi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,371 @@ | ||
| 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/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. | ||
| 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 <profile-name>. | ||
| func awsCredentialsCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "aws-credentials <profile-name>", | ||
| 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, ssoRegion) | ||
| 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) | ||
| } | ||
|
|
||
| pterm.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, 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 <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", ssoRegion, | ||
| "--output", "json") | ||
|
|
||
afalahi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) { | ||
| // 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 { | ||
| return false, fmt.Errorf("failed to get C1 client: %w", err) | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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 | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: AWS SSO Token Parsing Fails
The
getSSOTokenfunction incorrectly parses theexpiresAtfield from AWS SSO cache files. This field is a string (e.g., ending with "UTC") that is often not RFC3339, causingjson.Unmarshalto fail when converting it totime.Time. Consequently, valid SSO tokens are silently skipped, preventing theaws-credentialscommand from retrieving credentials and leading to repeated login prompts or failures.