Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/stackit_beta_intake_user_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ stackit beta intake user create [flags]
### Examples

```
Create a new Intake User with required parameters
$ stackit beta intake user create --display-name intake-user --intake-id xxx --password "SuperSafepass123\!"
Create a new Intake User. The password is entered interactively in the terminal
$ stackit beta intake user create --display-name intake-user --intake-id xxx

Create a new Intake User providing the password from a file
$ stackit beta intake user create --display-name intake-user --intake-id xxx --password @./secret.txt

Create a new Intake User for the dead-letter queue with labels
$ stackit beta intake user create --display-name dlq-user --intake-id xxx --password "SuperSafepass123\!" --type dead-letter --labels "env=prod"
$ stackit beta intake user create --display-name dlq-user --intake-id xxx --password @./secret.txt --type dead-letter --labels "env=prod"
```

### Options
Expand All @@ -28,7 +31,7 @@ stackit beta intake user create [flags]
-h, --help Help for "stackit beta intake user create"
--intake-id string The UUID of the Intake to associate the user with
--labels stringToString Labels in key=value format, separated by commas (default [])
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, number, and special characters (min 12 chars)
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, digits, and special characters (min 12 chars).
--type string Type of user. One of 'intake' (default) or 'dead-letter' (default "intake")
```

Expand Down
21 changes: 12 additions & 9 deletions docs/stackit_beta_intake_user_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,23 @@ stackit beta intake user update USER_ID [flags]
Update the display name of an Intake User
$ stackit beta intake user update xxx --intake-id yyy --display-name "new-user-name"

Update the password and description for an Intake User
$ stackit beta intake user update xxx --intake-id yyy --password "NewSecret123\!" --description "Updated description"
Update the password interactively for an Intake User
$ stackit beta intake user update xxx --intake-id yyy --password

Update the password and description for an Intake User from a file
$ stackit beta intake user update xxx --intake-id yyy --password @./secret.txt --description "Updated description"
```

### Options

```
--description string Description
--display-name string Display name
-h, --help Help for "stackit beta intake user update"
--intake-id string Intake ID
--labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default [])
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, number, and special characters (min 12 chars)
--type string Type of user. One of 'intake' or 'dead-letter'
--description string Description
--display-name string Display name
-h, --help Help for "stackit beta intake user update"
--intake-id string Intake ID
--labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default [])
--password string[="__INTERACTIVE__"] Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). If provided without a value, you will be prompted interactively. Must contain lower, upper, digits, and special characters (min 12 chars).
--type string Type of user. One of 'intake' or 'dead-letter'
```

### Options inherited from parent commands
Expand Down
46 changes: 40 additions & 6 deletions internal/cmd/beta/intake/user/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package create
import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"
intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi"
Expand Down Expand Up @@ -49,11 +50,14 @@ func NewCmd(p *types.CmdParams) *cobra.Command {
Args: args.NoArgs,
Example: examples.Build(
examples.NewExample(
`Create a new Intake User with required parameters`,
`$ stackit beta intake user create --display-name intake-user --intake-id xxx --password "SuperSafepass123\!"`),
`Create a new Intake User. The password is entered interactively in the terminal`,
`$ stackit beta intake user create --display-name intake-user --intake-id xxx`),
examples.NewExample(
`Create a new Intake User providing the password from a file`,
`$ stackit beta intake user create --display-name intake-user --intake-id xxx --password @./secret.txt`),
examples.NewExample(
`Create a new Intake User for the dead-letter queue with labels`,
`$ stackit beta intake user create --display-name dlq-user --intake-id xxx --password "SuperSafepass123\!" --type dead-letter --labels "env=prod"`),
`$ stackit beta intake user create --display-name dlq-user --intake-id xxx --password @./secret.txt --type dead-letter --labels "env=prod"`),
),
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := context.Background()
Expand Down Expand Up @@ -109,12 +113,12 @@ func configureFlags(cmd *cobra.Command, params *types.CmdParams) {
cmd.Flags().String(displayNameFlag, "", "Display name")
cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "The UUID of the Intake to associate the user with")
password := flags.SecretFlag(passwordFlag, params)
cmd.Flags().Var(password, passwordFlag, password.Usage()+" Must contain lower, upper, number, and special characters (min 12 chars)")
cmd.Flags().Var(password, passwordFlag, password.Usage()+" Must contain lower, upper, digits, and special characters (min 12 chars).")
cmd.Flags().String(userTypeFlag, string(intake.USERTYPE_INTAKE), "Type of user. One of 'intake' (default) or 'dead-letter'")
cmd.Flags().String(descriptionFlag, "", "Description")
cmd.Flags().StringToString(labelsFlag, nil, "Labels in key=value format, separated by commas")

err := flags.MarkFlagsRequired(cmd, displayNameFlag, intakeIdFlag, passwordFlag)
err := flags.MarkFlagsRequired(cmd, displayNameFlag, intakeIdFlag)
cobra.CheckErr(err)
}

Expand All @@ -124,11 +128,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
return nil, &cliErr.ProjectIdError{}
}

password, err := parsePassword(p, cmd)
if err != nil {
return nil, err
}

model := inputModel{
GlobalFlagModel: globalFlags,
DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag),
IntakeId: flags.FlagToStringPointer(p, cmd, intakeIdFlag),
Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag),
Password: password,
UserType: flags.FlagToStringPointer(p, cmd, userTypeFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag),
Expand All @@ -138,6 +147,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
return &model, nil
}

func parsePassword(p *print.Printer, cmd *cobra.Command) (*string, error) {
if cmd.Flag(passwordFlag).Changed {
val, err := cmd.Flags().GetString(passwordFlag)
if err != nil {
return nil, fmt.Errorf("reading password: %w", err)
}
val = strings.TrimRight(val, "\r\n")
if val == "" {
return nil, fmt.Errorf("the provided password (or secret file) is empty")
}
return &val, nil
}

password := flags.SecretFlagToStringPointer(p, cmd, passwordFlag)
if password != nil {
trimmed := strings.TrimRight(*password, "\r\n")
if trimmed == "" {
return nil, fmt.Errorf("password cannot be empty")
}
return &trimmed, nil
}

return nil, nil
}

func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiCreateIntakeUserRequest {
req := apiClient.DefaultAPI.CreateIntakeUser(ctx, model.ProjectId, model.Region, *model.IntakeId)

Expand Down
5 changes: 4 additions & 1 deletion internal/cmd/beta/intake/user/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ func TestParseInput(t *testing.T) {
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
delete(flagValues, passwordFlag)
}),
isValid: false,
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.Password = nil
}),
},
{
description: "required fields only",
Expand Down
100 changes: 86 additions & 14 deletions internal/cmd/beta/intake/user/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package update
import (
"context"
"fmt"
"io/fs"
"strings"

"github.com/spf13/cobra"
intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi"
Expand All @@ -29,8 +31,48 @@ const (
passwordFlag = "password"
userTypeFlag = "type"
labelsFlag = "labels"

interactivePasswordPlaceholder = "__INTERACTIVE__"
)

type secretUpdateFlag struct {
printer *print.Printer
fs fs.FS
value string
isPrompt bool
}

func (f *secretUpdateFlag) String() string {
return f.value
}

func (f *secretUpdateFlag) Set(value string) error {
if value == interactivePasswordPlaceholder {
f.isPrompt = true
return nil
}
if strings.HasPrefix(value, "@") {
path := strings.Trim(value[1:], `"'`)
bytes, err := fs.ReadFile(f.fs, path)
if err != nil {
return fmt.Errorf("reading secret %s: %w", passwordFlag, err)
}
val := strings.TrimRight(string(bytes), "\r\n")
if val == "" {
return fmt.Errorf("the provided secret file %q is empty", path)
}
f.value = val
return nil
}
f.printer.Warn("Passing a secret value on the command line is insecure and deprecated. This usage will stop working October 2026.\n")
f.value = value
return nil
}

func (f *secretUpdateFlag) Type() string {
return "string"
}

type inputModel struct {
*globalflags.GlobalFlagModel
IntakeId string
Expand All @@ -43,6 +85,11 @@ type inputModel struct {
}

func NewCmd(p *types.CmdParams) *cobra.Command {
password := &secretUpdateFlag{
printer: p.Printer,
fs: p.Fs,
}

cmd := &cobra.Command{
Use: fmt.Sprintf("update %s", userIdArg),
Short: "Updates an Intake User",
Expand All @@ -53,8 +100,11 @@ func NewCmd(p *types.CmdParams) *cobra.Command {
`Update the display name of an Intake User`,
`$ stackit beta intake user update xxx --intake-id yyy --display-name "new-user-name"`),
examples.NewExample(
`Update the password and description for an Intake User`,
`$ stackit beta intake user update xxx --intake-id yyy --password "NewSecret123\!" --description "Updated description"`),
`Update the password interactively for an Intake User`,
`$ stackit beta intake user update xxx --intake-id yyy --password`),
examples.NewExample(
`Update the password and description for an Intake User from a file`,
`$ stackit beta intake user update xxx --intake-id yyy --password @./secret.txt --description "Updated description"`),
),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
Expand Down Expand Up @@ -82,7 +132,6 @@ func NewCmd(p *types.CmdParams) *cobra.Command {
_, err = wait.UpdateIntakeUserWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.IntakeId, model.UserId).WaitWithContext(ctx)
return err
})

if err != nil {
return fmt.Errorf("wait for STACKIT Intake User update: %w", err)
}
Expand All @@ -91,16 +140,16 @@ func NewCmd(p *types.CmdParams) *cobra.Command {
return outputResult(p.Printer, model, resp)
},
}
configureFlags(cmd, p)
configureFlags(cmd, password)
return cmd
}

func configureFlags(cmd *cobra.Command, p *types.CmdParams) {
func configureFlags(cmd *cobra.Command, password *secretUpdateFlag) {
cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "Intake ID")
cmd.Flags().String(displayNameFlag, "", "Display name")
cmd.Flags().String(descriptionFlag, "", "Description")
password := flags.SecretFlag(passwordFlag, p)
cmd.Flags().Var(password, passwordFlag, password.Usage()+" Must contain lower, upper, number, and special characters (min 12 chars)")
cmd.Flags().Var(password, passwordFlag, "Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). If provided without a value, you will be prompted interactively. Must contain lower, upper, digits, and special characters (min 12 chars).")
cmd.Flags().Lookup(passwordFlag).NoOptDefVal = interactivePasswordPlaceholder
cmd.Flags().String(userTypeFlag, "", "Type of user. One of 'intake' or 'dead-letter'")
cmd.Flags().StringToString(labelsFlag, nil, `Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2".`)

Expand All @@ -116,13 +165,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu
return nil, &cliErr.ProjectIdError{}
}

password, err := parsePassword(p, cmd)
if err != nil {
return nil, err
}

model := &inputModel{
GlobalFlagModel: globalFlags,
IntakeId: flags.FlagToStringValue(p, cmd, intakeIdFlag),
UserId: userId,
DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag),
Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag),
Password: password,
UserType: flags.FlagToStringPointer(p, cmd, userTypeFlag),
Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag),
}
Expand All @@ -135,6 +189,29 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu
return model, nil
}

func parsePassword(p *print.Printer, cmd *cobra.Command) (*string, error) {
flag := cmd.Flag(passwordFlag)
if flag == nil || !flag.Changed {
return nil, nil
}
if secretFlag, ok := flag.Value.(*secretUpdateFlag); ok && secretFlag.isPrompt {
input, err := p.PromptForPassword("enter new password: ")
if err != nil {
return nil, fmt.Errorf("prompt for password: %w", err)
}
input = strings.TrimRight(input, "\r\n")
if input == "" {
return nil, fmt.Errorf("password cannot be empty")
}
return &input, nil
}
val := strings.TrimRight(flag.Value.String(), "\r\n")
if val == "" {
return nil, fmt.Errorf("the provided password (or secret file) is empty")
}
return &val, nil
}

func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiUpdateIntakeUserRequest {
req := apiClient.DefaultAPI.UpdateIntakeUser(ctx, model.ProjectId, model.Region, model.IntakeId, model.UserId)

Expand All @@ -156,16 +233,11 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIC

func outputResult(p *print.Printer, model *inputModel, resp *intake.IntakeUserResponse) error {
return p.OutputResult(model.OutputFormat, resp, func() error {
if resp == nil {
p.Outputf("Triggered update of Intake User for intake %q, but no user ID was returned.\n", model.IntakeId)
return nil
}

operationState := "Updated"
if model.Async {
operationState = "Triggered update of"
}
p.Outputf("%s Intake User for intake %q. User ID: %s\n", operationState, model.IntakeId, resp.Id)
p.Outputf("%s Intake User %s\n", operationState, model.UserId)
return nil
})
}