From 4ba2d4defcfb5c6fb4815cf9ff9787c5678720af Mon Sep 17 00:00:00 2001 From: JoshuaWilkes <14214200+JoshuaWilkes@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:39:43 +0800 Subject: [PATCH] Adds support for adding attachments to requests with Common Fate using --attach --- go.mod | 2 +- go.sum | 2 + pkg/assume/assume.go | 15 +-- pkg/assume/entrypoint.go | 1 + pkg/granted/eks/eks.go | 2 + pkg/granted/proxy/ensureaccess.go | 16 ++-- pkg/granted/rds/rds.go | 2 + pkg/granted/request/request.go | 10 +- .../accessrequesthook/accessrequesthook.go | 94 +++++++++++++------ 9 files changed, 98 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index c493b9e6..ee31ab2e 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/common-fate/common-fate v0.15.13 github.com/common-fate/glide-cli v0.6.0 github.com/common-fate/grab v1.3.0 - github.com/common-fate/sdk v1.69.0 + github.com/common-fate/sdk v1.70.3-0.20241125053707-a6c1defd9189 github.com/common-fate/xid v1.0.0 github.com/fatih/color v1.16.0 github.com/hashicorp/yamux v0.1.2 diff --git a/go.sum b/go.sum index 63f7cf55..372b1f56 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,8 @@ github.com/common-fate/iso8601 v1.1.0 h1:nrej9shsK1aB4IyOAjZl68xGk8yDuUxVwQjoDzx github.com/common-fate/iso8601 v1.1.0/go.mod h1:DU4mvUEkkWZUUSJq2aCuNqM1luSb0Pwyb2dLzXS+img= github.com/common-fate/sdk v1.69.0 h1:EcgIBjAFFvQnCd1/Lj5Wik/bOUMD9xhxDLEmXS1H7Gk= github.com/common-fate/sdk v1.69.0/go.mod h1:OrXhzB2Y1JSrKGHrb4qRmY+6MF2M3MFb+3edBnessXo= +github.com/common-fate/sdk v1.70.3-0.20241125053707-a6c1defd9189 h1:1MdYrkF18no04kC/VRrr4mqAaQMzHDINJ4jxtDvnoyk= +github.com/common-fate/sdk v1.70.3-0.20241125053707-a6c1defd9189/go.mod h1:OrXhzB2Y1JSrKGHrb4qRmY+6MF2M3MFb+3edBnessXo= github.com/common-fate/session-manager-plugin v0.0.0-20240723053832-3d311db99016 h1:WObxQKT/BuR8HWKSGsJ6aQb/cdhvkenkb1KWXNyPWeE= github.com/common-fate/session-manager-plugin v0.0.0-20240723053832-3d311db99016/go.mod h1:glAZTUB+4Eg0JVLC3B/YEomJv6QHcNS3klJjw9HC5Y8= github.com/common-fate/updatecheck v0.3.5 h1:UGIKMnYwuHjbhhCaisLz1pNPg8Z1nXEoWcfqT+4LkAg= diff --git a/pkg/assume/assume.go b/pkg/assume/assume.go index a30c0b27..515a8769 100644 --- a/pkg/assume/assume.go +++ b/pkg/assume/assume.go @@ -305,7 +305,7 @@ func AssumeCommand(c *cli.Context) error { } reason := assumeFlags.String("reason") - + attachments := assumeFlags.StringSlice("attach") cfg, err := config.Load() if err != nil { return err @@ -350,12 +350,13 @@ func AssumeCommand(c *cli.Context) error { } noAccessInput := accessrequesthook.NoAccessInput{ - Profile: profile, - Reason: reason, - Duration: apiDuration, - Confirm: assumeFlags.Bool("confirm"), - Wait: wait, - StartTime: time.Now(), + Profile: profile, + Reason: reason, + Attachments: attachments, + Duration: apiDuration, + Confirm: assumeFlags.Bool("confirm"), + Wait: wait, + StartTime: time.Now(), } retry, justActivated, hookErr := hook.NoAccess(c.Context, noAccessInput) if hookErr != nil { diff --git a/pkg/assume/entrypoint.go b/pkg/assume/entrypoint.go index d7817844..84c765ba 100644 --- a/pkg/assume/entrypoint.go +++ b/pkg/assume/entrypoint.go @@ -58,6 +58,7 @@ func GlobalFlags() []cli.Flag { &cli.BoolFlag{Name: "no-cache", Usage: "Disables caching of session credentials and forces a refresh", EnvVars: []string{"GRANTED_NO_CACHE"}}, &cli.StringSliceFlag{Name: "browser-launch-template-arg", Usage: "Additional arguments to provide to the browser launch template command in key=value format, e.g. '--browser-launch-template-arg foo=bar"}, &cli.BoolFlag{Name: "skip-profile-registry-sync", Usage: "You can use this to skip the automated profile registry sync process."}, + &cli.StringSliceFlag{Name: "attach", Usage: "Attach justifications to your request, such as a Jira ticket id or url `--attach=TP-123`"}, } } diff --git a/pkg/granted/eks/eks.go b/pkg/granted/eks/eks.go index 6586ca68..ad6bb6e8 100644 --- a/pkg/granted/eks/eks.go +++ b/pkg/granted/eks/eks.go @@ -36,6 +36,7 @@ var proxyCommand = cli.Command{ &cli.StringFlag{Name: "target", Aliases: []string{"cluster"}}, &cli.StringFlag{Name: "role", Aliases: []string{"service-account"}}, &cli.StringFlag{Name: "reason", Usage: "Provide a reason for requesting access to the role"}, + &cli.StringSliceFlag{Name: "attach", Usage: "Attach justifications to your request, such as a Jira ticket id or url `--attach=TP-123`"}, &cli.BoolFlag{Name: "confirm", Aliases: []string{"y"}, Usage: "Skip confirmation prompts for access requests"}, &cli.BoolFlag{Name: "wait", Value: true, Usage: "Wait for the access request to be approved."}, &cli.BoolFlag{Name: "no-cache", Usage: "Disables caching of session credentials and forces a refresh", EnvVars: []string{"GRANTED_NO_CACHE"}}, @@ -59,6 +60,7 @@ var proxyCommand = cli.Command{ Role: c.String("role"), Duration: c.Duration("duration"), Reason: c.String("reason"), + Attachments: c.StringSlice("attach"), Confirm: c.Bool("confirm"), Wait: c.Bool("wait"), PromptForEntitlement: promptForClusterAndRole, diff --git a/pkg/granted/proxy/ensureaccess.go b/pkg/granted/proxy/ensureaccess.go index 459fcac3..e704501c 100644 --- a/pkg/granted/proxy/ensureaccess.go +++ b/pkg/granted/proxy/ensureaccess.go @@ -28,6 +28,7 @@ type EnsureAccessInput[T any] struct { Role string Duration time.Duration Reason string + Attachments []string Confirm bool Wait bool PromptForEntitlement func(ctx context.Context, cfg *config.Context) (*accessv1alpha1.Entitlement, error) @@ -42,13 +43,14 @@ type EnsureAccessOutput[T any] struct { func EnsureAccess[T any](ctx context.Context, cfg *config.Context, input EnsureAccessInput[T]) (*EnsureAccessOutput[T], error) { accessRequestInput := accessrequesthook.NoEntitlementAccessInput{ - Target: input.Target, - Role: input.Role, - Reason: input.Reason, - Duration: durationOrDefault(input.Duration), - Confirm: input.Confirm, - Wait: input.Wait, - StartTime: time.Now(), + Target: input.Target, + Role: input.Role, + Reason: input.Reason, + Attachments: input.Attachments, + Duration: durationOrDefault(input.Duration), + Confirm: input.Confirm, + Wait: input.Wait, + StartTime: time.Now(), } if accessRequestInput.Target == "" && accessRequestInput.Role == "" { diff --git a/pkg/granted/rds/rds.go b/pkg/granted/rds/rds.go index 12648706..43de083a 100644 --- a/pkg/granted/rds/rds.go +++ b/pkg/granted/rds/rds.go @@ -39,6 +39,7 @@ var proxyCommand = cli.Command{ &cli.StringFlag{Name: "role", Aliases: []string{"user"}}, &cli.IntFlag{Name: "port", Usage: "The local port to forward the database connection to"}, &cli.StringFlag{Name: "reason", Usage: "Provide a reason for requesting access to the role"}, + &cli.StringSliceFlag{Name: "attach", Usage: "Attach justifications to your request, such as a Jira ticket id or url `--attach=TP-123`"}, &cli.BoolFlag{Name: "confirm", Aliases: []string{"y"}, Usage: "Skip confirmation prompts for access requests"}, &cli.BoolFlag{Name: "wait", Value: true, Usage: "Wait for the access request to be approved."}, &cli.BoolFlag{Name: "no-cache", Usage: "Disables caching of session credentials and forces a refresh", EnvVars: []string{"GRANTED_NO_CACHE"}}, @@ -62,6 +63,7 @@ var proxyCommand = cli.Command{ Role: c.String("role"), Duration: c.Duration("duration"), Reason: c.String("reason"), + Attachments: c.StringSlice("attach"), Confirm: c.Bool("confirm"), Wait: c.Bool("wait"), PromptForEntitlement: promptForDatabaseAndUser, diff --git a/pkg/granted/request/request.go b/pkg/granted/request/request.go index 27602efe..94062e38 100644 --- a/pkg/granted/request/request.go +++ b/pkg/granted/request/request.go @@ -36,6 +36,7 @@ var latestCommand = cli.Command{ Usage: "Request access to the latest AWS role you attempted to use", Flags: []cli.Flag{ &cli.StringFlag{Name: "reason", Usage: "A reason for access"}, + &cli.StringSliceFlag{Name: "attach", Usage: "Attach justifications to your request, such as a Jira ticket id or url `--attach=TP-123`"}, &cli.DurationFlag{Name: "duration", Usage: "Duration of request, defaults to max duration of the access rule."}, &cli.BoolFlag{Name: "confirm", Aliases: []string{"y"}, Usage: "Skip confirmation prompts for access requests"}, }, @@ -105,10 +106,11 @@ var latestCommand = cli.Command{ } _, _, err = hook.NoAccess(c.Context, accessrequesthook.NoAccessInput{ - Profile: profile, - Reason: reason, - Duration: apiDuration, - Confirm: c.Bool("confirm"), + Profile: profile, + Reason: reason, + Attachments: c.StringSlice("attach"), + Duration: apiDuration, + Confirm: c.Bool("confirm"), }) if err != nil { return err diff --git a/pkg/hook/accessrequesthook/accessrequesthook.go b/pkg/hook/accessrequesthook/accessrequesthook.go index 059477ce..03d116b0 100644 --- a/pkg/hook/accessrequesthook/accessrequesthook.go +++ b/pkg/hook/accessrequesthook/accessrequesthook.go @@ -14,6 +14,7 @@ import ( "github.com/briandowns/spinner" "github.com/common-fate/cli/printdiags" "github.com/common-fate/clio" + "github.com/common-fate/grab" "github.com/common-fate/granted/pkg/cfaws" "github.com/common-fate/granted/pkg/cfcfg" "github.com/common-fate/sdk/config" @@ -31,12 +32,13 @@ import ( type Hook struct{} type NoAccessInput struct { - Profile *cfaws.Profile - Reason string - Duration *durationpb.Duration - Confirm bool - Wait bool - StartTime time.Time + Profile *cfaws.Profile + Reason string + Attachments []string + Duration *durationpb.Duration + Confirm bool + Wait bool + StartTime time.Time } func (h Hook) NoAccess(ctx context.Context, input NoAccessInput) (retry bool, justActivated bool, err error) { @@ -53,26 +55,28 @@ func (h Hook) NoAccess(ctx context.Context, input NoAccessInput) (retry bool, ju clio.Infof("You don't currently have access to %s, checking if we can request access...\t[target=%s, role=%s, url=%s]", input.Profile.Name, target, role, cfg.AccessURL) retry, _, justActivated, err = h.NoEntitlementAccess(ctx, cfg, NoEntitlementAccessInput{ - Target: target.String(), - Role: role, - Reason: input.Reason, - Duration: input.Duration, - Confirm: input.Confirm, - Wait: input.Wait, - StartTime: input.StartTime, + Target: target.String(), + Role: role, + Reason: input.Reason, + Duration: input.Duration, + Confirm: input.Confirm, + Wait: input.Wait, + StartTime: input.StartTime, + Attachments: input.Attachments, }) return retry, justActivated, err } type NoEntitlementAccessInput struct { - Target string - Role string - Reason string - Duration *durationpb.Duration - Confirm bool - Wait bool - StartTime time.Time + Target string + Role string + Reason string + Attachments []string + Duration *durationpb.Duration + Confirm bool + Wait bool + StartTime time.Time } func (h Hook) NoEntitlementAccess(ctx context.Context, cfg *config.Context, input NoEntitlementAccessInput) (retry bool, result *accessv1alpha1.BatchEnsureResponse, justActivated bool, err error) { @@ -167,6 +171,41 @@ func (h Hook) NoEntitlementAccess(ctx context.Context, cfg *config.Context, inpu } } + if len(input.Attachments) > 0 { + req.Justification.Attachments = grab.Map(input.Attachments, func(t string) *accessv1alpha1.AttachmentSpecifier { + return &accessv1alpha1.AttachmentSpecifier{ + Specify: &accessv1alpha1.AttachmentSpecifier_Lookup{ + Lookup: t, + }, + } + }) + } else { + if result.Validation != nil && result.Validation.HasJiraTicket { + if !IsTerminal(os.Stdin.Fd()) { + return false, nil, justActivated, errors.New("detected a noninteractive terminal: a jira ticket attachment is required to make this access request, to apply the planned changes please re-run with the --attach flag") + } + + var attachment string + msg := "Jira ticket attachment for access (Required)" + reasonPrompt := &survey.Input{ + Message: msg, + Help: "Will be stored in audit trails and associated with your request", + } + withStdio := survey.WithStdio(os.Stdin, os.Stderr, os.Stderr) + err = survey.AskOne(reasonPrompt, &attachment, withStdio, survey.WithValidator(survey.Required)) + + if err != nil { + return false, nil, justActivated, err + } + + req.Justification.Attachments = append(req.Justification.Attachments, &accessv1alpha1.AttachmentSpecifier{ + Specify: &accessv1alpha1.AttachmentSpecifier_Lookup{ + Lookup: attachment, + }, + }) + } + } + // the spinner must be started after prompting for reason, otherwise the prompt gets hidden si := spinner.New(spinner.CharSets[14], 100*time.Millisecond) si.Suffix = " ensuring access..." @@ -276,13 +315,14 @@ func (h Hook) RetryAccess(ctx context.Context, input NoAccessInput) error { target := eid.New("AWS::Account", input.Profile.AWSConfig.SSOAccountID) role := input.Profile.AWSConfig.SSORoleName _, err = h.RetryNoEntitlementAccess(ctx, cfg, NoEntitlementAccessInput{ - Target: target.String(), - Role: role, - Reason: input.Reason, - Duration: input.Duration, - Confirm: input.Confirm, - Wait: input.Wait, - StartTime: input.StartTime, + Target: target.String(), + Role: role, + Reason: input.Reason, + Duration: input.Duration, + Confirm: input.Confirm, + Wait: input.Wait, + StartTime: input.StartTime, + Attachments: input.Attachments, }) return err }