From b289978e2ea96ccc1d913f20a3d97e76f57f6a20 Mon Sep 17 00:00:00 2001 From: Guillaume Alaux Date: Wed, 1 Apr 2026 08:13:27 +0200 Subject: [PATCH] "Assume role" full support added Modified commands - `iam role create`: supports two new fields - `iam role update`: supports two new fields - `iam role show`: supports two new fields - `iam role assume`: entirely new --- cmd/iam/iam_role_assume.go | 87 +++++++++++++++++++++++++++++++++++++ cmd/iam/iam_role_create.go | 54 +++++++++++++++++------ cmd/iam/iam_role_show.go | 62 ++++++++++++++++++++------ cmd/iam/iam_role_update.go | 89 ++++++++++++++++++++++++-------------- 4 files changed, 233 insertions(+), 59 deletions(-) create mode 100644 cmd/iam/iam_role_assume.go diff --git a/cmd/iam/iam_role_assume.go b/cmd/iam/iam_role_assume.go new file mode 100644 index 000000000..efc020c4f --- /dev/null +++ b/cmd/iam/iam_role_assume.go @@ -0,0 +1,87 @@ +package iam + +import ( + "errors" + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/account" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + v3 "github.com/exoscale/egoscale/v3" +) + +type iamRoleAssumeOutput struct { + Key string `json:"key"` + Name string `json:"name"` + OrgID string `json:"org-id"` + RoleID string `json:"role-id"` + Secret string `json:"secret"` +} + +func (o *iamRoleAssumeOutput) ToJSON() { output.JSON(o) } +func (o *iamRoleAssumeOutput) ToText() { output.Text(o) } +func (o *iamRoleAssumeOutput) ToTable() { output.Table(o) } + +type iamRoleAssumeCmd struct { + exocmd.CliCommandSettings `cli-cmd:"-"` + + Role string `cli-arg:"#" cli-usage:"ID|NAME"` + + Ttl int64 `cli-flag:"ttl" cli-usage:"Time To Live for the requested key in seconds (default: 300)"` + + _ bool `cli-cmd:"assume"` +} + +func (c *iamRoleAssumeCmd) CmdAliases() []string { return nil } + +func (c *iamRoleAssumeCmd) CmdShort() string { + return "Request generation of key/secret allowing calls as of target role" +} + +func (c *iamRoleAssumeCmd) CmdLong() string { + return "Request generation of key/secret allowing calls as of target role" +} + +func (c *iamRoleAssumeCmd) CmdPreRun(cmd *cobra.Command, args []string) error { + return exocmd.CliCommandDefaultPreRun(c, cmd, args) +} + +func (c *iamRoleAssumeCmd) CmdRun(cmd *cobra.Command, _ []string) error { + if c.Role == "" { + return errors.New("role not provided") + } + + ctx := exocmd.GContext + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(account.CurrentAccount.DefaultZone)) + if err != nil { + return err + } + + assumeRoleRq := v3.AssumeIAMRoleRequest{} + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.Ttl)) { + assumeRoleRq.Ttl = c.Ttl + } + + apiKey, err := client.AssumeIAMRole(ctx, v3.UUID(c.Role), assumeRoleRq) + if err != nil { + return err + } + + out := iamRoleAssumeOutput{ + Key: apiKey.Key, + Name: apiKey.Name, + OrgID: apiKey.OrgID, + RoleID: apiKey.RoleID, + Secret: apiKey.Secret, + } + + return c.OutputFunc(&out, nil) +} + +func init() { + cobra.CheckErr(exocmd.RegisterCLICommand(iamRoleCmd, &iamRoleAssumeCmd{ + CliCommandSettings: exocmd.DefaultCLICmdSettings(), + })) +} diff --git a/cmd/iam/iam_role_create.go b/cmd/iam/iam_role_create.go index d34e24b8a..077cae22c 100644 --- a/cmd/iam/iam_role_create.go +++ b/cmd/iam/iam_role_create.go @@ -21,12 +21,14 @@ type iamRoleCreateCmd struct { _ bool `cli-cmd:"create"` - Name string `cli-arg:"#" cli-usage:"NAME"` - Description string `cli-flag:"description" cli-usage:"Role description"` - Permissions []string `cli-flag:"permissions" cli-usage:"Role permissions"` - Editable bool `cli-flag:"editable" cli-usage:"Set --editable=false do prevent editing Policy after creation"` - Labels map[string]string `cli-flag:"label" cli-usage:"Role labels (format: key=value)"` - Policy string `cli-flag:"policy" cli-usage:"Role policy (use '-' to read from STDIN)"` + Name string `cli-arg:"#" cli-usage:"NAME"` + Description string `cli-flag:"description" cli-usage:"Role description"` + Permissions []string `cli-flag:"permissions" cli-usage:"Role permissions"` + Editable bool `cli-flag:"editable" cli-usage:"Set --editable=false do prevent editing Policy after creation"` + Labels map[string]string `cli-flag:"label" cli-usage:"Role labels (format: key=value)"` + Policy string `cli-flag:"policy" cli-usage:"Role policy (use '-' to read from STDIN)"` + AssumeRolePolicy string `cli-flag:"assume-role-policy" cli-usage:"Assume Role policy (use '-' to read from STDIN)"` + MaxSessionTtl int64 `cli-flag:"max-session-ttl" cli-usage:"Maximum TTL requester is allowed to ask for when assuming a role (0 implies default)"` } func (c *iamRoleCreateCmd) CmdAliases() []string { return nil } @@ -37,9 +39,9 @@ func (c *iamRoleCreateCmd) CmdShort() string { func (c *iamRoleCreateCmd) CmdLong() string { return fmt.Sprintf(`This command creates a new IAM Role. -To read the Policy from STDIN, append '-' to the '--policy' flag. +To read a policy from STDIN, append '-' to the '--policy' or '--assume-role-policy' flag. -Pro Tip: you can reuse an existing role policy by providing the output of the show command as input: +Pro Tip: you can reuse an existing policy by providing the output of the show command as input: exo iam role show --policy --output-format json | exo iam role create --policy - @@ -63,6 +65,7 @@ func (c *iamRoleCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { } var policy *v3.IAMPolicy + var assumeRolePolicy *v3.IAMPolicy // Policy is optional, if not set API will default to `allow all` if c.Policy != "" { @@ -85,18 +88,43 @@ func (c *iamRoleCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { } } + if c.AssumeRolePolicy != "" { + // If Assume Role Policy value is `-` read from STDIN + if c.AssumeRolePolicy == "-" { + inputReader := cmd.InOrStdin() + b, err := io.ReadAll(inputReader) + if err != nil { + return fmt.Errorf("failed to read assume role policy from stdin: %w", err) + } + + c.AssumeRolePolicy = string(b) + } + + var err error + + assumeRolePolicy, err = iamPolicyFromJSON([]byte(c.AssumeRolePolicy)) + if err != nil { + return fmt.Errorf("failed to parse IAM policy: %w", err) + } + } + role := v3.CreateIAMRoleRequest{ - Name: c.Name, - Editable: &c.Editable, - Labels: c.Labels, - Permissions: c.Permissions, - Policy: policy, + Name: c.Name, + Editable: &c.Editable, + Labels: c.Labels, + Permissions: c.Permissions, + Policy: policy, + AssumeRolePolicy: assumeRolePolicy, } if c.Description != "" { role.Description = c.Description } + if c.MaxSessionTtl != 0 { + role.MaxSessionTtl = c.MaxSessionTtl + } + op, err := client.CreateIAMRole(ctx, role) if err != nil { return err diff --git a/cmd/iam/iam_role_show.go b/cmd/iam/iam_role_show.go index 133e5ce62..ed950091a 100644 --- a/cmd/iam/iam_role_show.go +++ b/cmd/iam/iam_role_show.go @@ -16,12 +16,13 @@ import ( ) type iamRoleShowOutput struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Editable bool `json:"editable"` - Labels map[string]string `json:"labels"` - Permissions []string `json:"permission"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Editable bool `json:"editable"` + Labels map[string]string `json:"labels"` + Permissions []string `json:"permission"` + MaxSessionTtl int64 `json:"max-session-ttl"` } func (o *iamRoleShowOutput) ToJSON() { output.JSON(o) } @@ -33,7 +34,8 @@ type iamRoleShowCmd struct { _ bool `cli-cmd:"show"` - Policy bool `cli-flag:"policy" cli-usage:"Print IAM Role policy"` + Policy bool `cli-flag:"policy" cli-usage:"Print IAM Role policy"` + AssumeRolePolicy bool `cli-flag:"assume-role-policy" cli-usage:"Print IAM Assume Role policy"` Role string `cli-arg:"#" cli-usage:"ID|NAME"` } @@ -104,13 +106,47 @@ func (c *iamRoleShowCmd) CmdRun(_ *cobra.Command, _ []string) error { return c.OutputFunc(&out, nil) } + if c.AssumeRolePolicy { + + if role.AssumeRolePolicy == nil { + return nil + } + + policy := role.AssumeRolePolicy + + out := iamPolicyOutput{ + DefaultServiceStrategy: string(policy.DefaultServiceStrategy), + Services: map[string]iamPolicyServiceOutput{}, + } + + for name, service := range policy.Services { + rules := []iamPolicyServiceRuleOutput{} + if service.Type == "rules" { + for _, rule := range service.Rules { + rules = append(rules, iamPolicyServiceRuleOutput{ + Action: string(rule.Action), + Expression: rule.Expression, + }) + } + } + + out.Services[name] = iamPolicyServiceOutput{ + Type: string(service.Type), + Rules: rules, + } + } + + return c.OutputFunc(&out, nil) + } + out := iamRoleShowOutput{ - ID: role.ID.String(), - Description: role.Description, - Editable: utils.DefaultBool(role.Editable, false), - Labels: role.Labels, - Name: role.Name, - Permissions: role.Permissions, + ID: role.ID.String(), + Description: role.Description, + Editable: utils.DefaultBool(role.Editable, false), + Labels: role.Labels, + Name: role.Name, + Permissions: role.Permissions, + MaxSessionTtl: role.MaxSessionTtl, } return c.OutputFunc(&out, nil) diff --git a/cmd/iam/iam_role_update.go b/cmd/iam/iam_role_update.go index 67161c71f..51c002bac 100644 --- a/cmd/iam/iam_role_update.go +++ b/cmd/iam/iam_role_update.go @@ -21,10 +21,12 @@ type iamRoleUpdateCmd struct { Role string `cli-arg:"#" cli-usage:"ID|NAME"` - Description string `cli-flag:"description" cli-usage:"Role description"` - Permissions []string `cli-flag:"permissions" cli-usage:"Role permissions"` - Labels map[string]string `cli-flag:"label" cli-usage:"Role labels (format: key=value)"` - Policy string `cli-flag:"policy" cli-usage:"Role policy (use '-' to read from STDIN)"` + Description string `cli-flag:"description" cli-usage:"Role description"` + Permissions []string `cli-flag:"permissions" cli-usage:"Role permissions"` + Labels map[string]string `cli-flag:"label" cli-usage:"Role labels (format: key=value)"` + Policy string `cli-flag:"policy" cli-usage:"Role policy (use '-' to read from STDIN)"` + AssumeRolePolicy string `cli-flag:"assume-role-policy" cli-usage:"Assume Role policy (use '-' to read from STDIN)"` + MaxSessionTtl int64 `cli-flag:"max-session-ttl" cli-usage:"Maximum TTL requester is allowed to ask for when assuming a role"` _ bool `cli-cmd:"update"` } @@ -37,7 +39,7 @@ func (c *iamRoleUpdateCmd) CmdShort() string { func (c *iamRoleUpdateCmd) CmdLong() string { return fmt.Sprintf(`This command updates an IAM Role. -When you supply '-' as a flag argument to '--policy', the new policy will be read from STDIN. +To read a policy from STDIN, append '-' to the '--policy' or '--assume-role-policy' flag. Supported output template annotations: %s`, strings.Join(output.TemplateAnnotations(&iamPolicyOutput{}), ", ")) @@ -78,6 +80,9 @@ func (c *iamRoleUpdateCmd) CmdRun(cmd *cobra.Command, _ []string) error { if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.Permissions)) { updateRole.Permissions = c.Permissions } + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.MaxSessionTtl)) { + updateRole.MaxSessionTtl = c.MaxSessionTtl + } op, err := client.UpdateIAMRole(ctx, role.ID, updateRole) if err != nil { @@ -90,42 +95,60 @@ func (c *iamRoleUpdateCmd) CmdRun(cmd *cobra.Command, _ []string) error { return err } - // If we don't need to update Policy we can exit now - if c.Policy == "" { - if !globalstate.Quiet { - return (&iamRoleShowCmd{ - CliCommandSettings: c.CliCommandSettings, - Role: role.ID.String(), - }).CmdRun(nil, nil) - } + if c.Policy != "" { + if c.Policy == "-" { + inputReader := cmd.InOrStdin() + b, err := io.ReadAll(inputReader) + if err != nil { + return fmt.Errorf("failed to read policy from stdin: %w", err) + } - return nil - } + c.Policy = string(b) + } - if c.Policy == "-" { - inputReader := cmd.InOrStdin() - b, err := io.ReadAll(inputReader) + policy, err := iamPolicyFromJSON([]byte(c.Policy)) if err != nil { - return fmt.Errorf("failed to read policy from stdin: %w", err) + return fmt.Errorf("failed to parse IAM policy: %w", err) } - c.Policy = string(b) + op, err = client.UpdateIAMRolePolicy(ctx, role.ID, *policy) + if err != nil { + return err + } + utils.DecorateAsyncOperation("Updating IAM role policy...", func() { + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + if err != nil { + return err + } } - policy, err := iamPolicyFromJSON([]byte(c.Policy)) - if err != nil { - return fmt.Errorf("failed to parse IAM policy: %w", err) - } + if c.AssumeRolePolicy != "" { + if c.AssumeRolePolicy == "-" { + inputReader := cmd.InOrStdin() + b, err := io.ReadAll(inputReader) + if err != nil { + return fmt.Errorf("failed to read assume role policy from stdin: %w", err) + } - op, err = client.UpdateIAMRolePolicy(ctx, role.ID, *policy) - if err != nil { - return err - } - utils.DecorateAsyncOperation("Updating IAM role policy...", func() { - _, err = client.Wait(ctx, op, v3.OperationStateSuccess) - }) - if err != nil { - return err + c.AssumeRolePolicy = string(b) + } + + assumeRolePolicy, err := iamPolicyFromJSON([]byte(c.AssumeRolePolicy)) + if err != nil { + return fmt.Errorf("failed to parse IAM assume role policy: %w", err) + } + + op, err = client.UpdateIAMRoleAssumePolicy(ctx, role.ID, *assumeRolePolicy) + if err != nil { + return err + } + utils.DecorateAsyncOperation("Updating IAM assume role policy...", func() { + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + if err != nil { + return err + } } if !globalstate.Quiet {