Skip to content

feat: add AWS credential mode and External ID support#585

Open
munishchouhan wants to merge 4 commits intomasterfrom
260212-aws-credential-modes
Open

feat: add AWS credential mode and External ID support#585
munishchouhan wants to merge 4 commits intomasterfrom
260212-aws-credential-modes

Conversation

@munishchouhan
Copy link
Member

Summary

  • Add --mode (keys|role) and --generate-external-id options to credentials add/update aws commands
  • Role mode requires --assume-role-arn, rejects --access-key/--secret-key, and auto-generates an External ID
  • Keys mode supports optional --generate-external-id with --assume-role-arn
  • Upgrade tower-java-sdk from 1.107.0 to 1.114.0

Files changed (12)

  • AwsProvider.java — New --mode, --generate-external-id options, validation, useExternalId() method
  • CredentialsProvider.java — Added default Boolean useExternalId() interface method
  • AbstractAddCmd.java / AbstractUpdateCmd.java — Pass useExternalId to API calls
  • libs.versions.toml / build.gradle — SDK 1.114.0, added jackson-databind-nullable
  • VERSION-API — Bumped to 1.114.0
  • AwsProviderTest.java — 7 new test cases (modes, external ID, validation errors)
  • InfoCmdTest.java / service-info.json — Updated mock API version
  • USAGE.md — Documented keys and role modes

Test plan

  • 499 tests pass (0 failures)
  • Native image compiles and validates correctly
  • CLI validation verified: role mode rejects keys, requires role ARN, invalid mode rejected

@munishchouhan munishchouhan self-assigned this Feb 26, 2026
@pditommaso
Copy link
Contributor

Unrelated but worth to update also the MCP https://github.com/seqeralabs/mcp

@munishchouhan
Copy link
Member Author

Tested successfully in local:

tw-cli % /bin/bash /Users/munish.chouhan/testing_ground/wave_testing/tw-cli/externalid-test.sh
============================================================
  AWS Credential Mode & External ID - CLI Tests
============================================================

>>> SECTION 1: Offline validation tests

------------------------------------------------------------
TEST #1: Help shows --mode and --generate-external-id
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --help

PASS (exit=0)
  Output: 
Usage: tw credentials add aws [OPTIONS]

Add AWS credentials

Options:
* -n, --name=<name>                       Credentials name.
  -w, --workspace=<workspace>             Workspace numeric identifier or reference in OrganizationName/WorkspaceName format (defaults to TOWER_WORKSPACE_ID
                                            environment variable)
      --overwrite                         Overwrite the credentials if it already exists.
  -a, --access-key=<accessKey>            AWS access key identifier. Part of AWS IAM credentials used for programmatic access to AWS services.
  -s, --secret-key=<secretKey>            AWS secret access key. Part of AWS IAM credentials used for programmatic access to AWS services. Keep this value
                                            secure.
  -r, --assume-role-arn=<assumeRoleArn>   IAM role ARN to assume for accessing AWS resources. Allows cross-account access or privilege elevation. Must be a
                                            fully qualified ARN (e.g., arn:aws:iam::123456789012:role/RoleName).
      --mode=<mode>                       AWS credential mode: 'keys' (access key + secret key) or 'role' (IAM role only). Default: keys.
      --generate-external-id              Generate a platform-managed External ID for the credential (used with IAM role ARN).
  -h, --help                              Show this help message and exit.
  -V, --version                           Print version information and exit.

------------------------------------------------------------
TEST #2: Role mode rejects --access-key/--secret-key
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=test-bad --mode=role -a AKIAIOSFODNN7EXAMPLE -s secret -r arn:aws:iam::123456789012:role/MyRole

PASS (expected failure, exit=1)
  Output: java.lang.IllegalArgumentException: Options '--access-key' and '--secret-key' cannot be used with '--mode=role'. Role mode uses IAM role assumption without static credentials.
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.validate(AwsProvider.java:92)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.securityKeys(AwsProvider.java:45)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.securityKeys(AwsProvider.java:25)
        at io.seqera.tower.cli.commands.credentials.add.AbstractAddCmd.exec(AbstractAddCmd.java:54)
        at io.seqera.tower.cli.commands.AbstractApiCmd.call(AbstractApiCmd.java:598)
        at io.seqera.tower.cli.commands.AbstractApiCmd.call(AbstractApiCmd.java:91)
        at picocli.CommandLine.executeUserObject(CommandLine.java:1953)
        at picocli.CommandLine.access$1300(CommandLine.java:145)
        at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2358)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2352)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2314)
        at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2179)
        at picocli.CommandLine$RunLast.execute(CommandLine.java:2316)
        at picocli.CommandLine.execute(CommandLine.java:2078)
        at io.seqera.tower.cli.Tower.main(Tower.java:104)
        at java.base@21.0.4/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)

------------------------------------------------------------
TEST #3: Role mode requires --assume-role-arn
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=test-bad --mode=role

PASS (expected failure, exit=1)
  Output: java.lang.IllegalArgumentException: Option '--assume-role-arn' is required when using '--mode=role'.
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.validate(AwsProvider.java:95)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.securityKeys(AwsProvider.java:45)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.securityKeys(AwsProvider.java:25)
        at io.seqera.tower.cli.commands.credentials.add.AbstractAddCmd.exec(AbstractAddCmd.java:54)
        at io.seqera.tower.cli.commands.AbstractApiCmd.call(AbstractApiCmd.java:598)
        at io.seqera.tower.cli.commands.AbstractApiCmd.call(AbstractApiCmd.java:91)
        at picocli.CommandLine.executeUserObject(CommandLine.java:1953)
        at picocli.CommandLine.access$1300(CommandLine.java:145)
        at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2358)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2352)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2314)
        at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2179)
        at picocli.CommandLine$RunLast.execute(CommandLine.java:2316)
        at picocli.CommandLine.execute(CommandLine.java:2078)
        at io.seqera.tower.cli.Tower.main(Tower.java:104)
        at java.base@21.0.4/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)

------------------------------------------------------------
TEST #4: Invalid mode value rejected
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=test-bad --mode=invalid -r arn:aws:iam::123456789012:role/MyRole

PASS (expected failure, exit=1)
  Output: java.lang.IllegalArgumentException: Invalid AWS credential mode 'invalid'. Allowed values: 'keys', 'role'.
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.getMode(AwsProvider.java:83)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.validate(AwsProvider.java:88)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.securityKeys(AwsProvider.java:45)
        at io.seqera.tower.cli.commands.credentials.providers.AwsProvider.securityKeys(AwsProvider.java:25)
        at io.seqera.tower.cli.commands.credentials.add.AbstractAddCmd.exec(AbstractAddCmd.java:54)
        at io.seqera.tower.cli.commands.AbstractApiCmd.call(AbstractApiCmd.java:598)
        at io.seqera.tower.cli.commands.AbstractApiCmd.call(AbstractApiCmd.java:91)
        at picocli.CommandLine.executeUserObject(CommandLine.java:1953)
        at picocli.CommandLine.access$1300(CommandLine.java:145)
        at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2358)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2352)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2314)
        at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2179)
        at picocli.CommandLine$RunLast.execute(CommandLine.java:2316)
        at picocli.CommandLine.execute(CommandLine.java:2078)
        at io.seqera.tower.cli.Tower.main(Tower.java:104)
        at java.base@21.0.4/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH)

>>> SECTION 2: Positive tests (requires TOWER_ACCESS_TOKEN)

------------------------------------------------------------
TEST #5: Add credentials - keys mode (implicit)
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-keys-implicit -a AKIAIOSFODNN7EXAMPLE -s wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

PASS (exit=0)
  Output: 
  New AWS credentials 'cli-test-keys-implicit (6v46nMt7xDhj7DqSnnYNhu)' added at user workspace

------------------------------------------------------------
TEST #6: Add credentials - keys mode (explicit --mode=keys)
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-keys-explicit --mode=keys -a AKIAIOSFODNN7EXAMPLE -s wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

PASS (exit=0)
  Output: 
  New AWS credentials 'cli-test-keys-explicit (4VNWeM0iE0WFNIF9vINMEa)' added at user workspace

------------------------------------------------------------
TEST #7: Add credentials - keys mode with assume-role-arn
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-keys-with-role -a AKIAIOSFODNN7EXAMPLE -s wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -r arn:aws:iam::123456789012:role/MyRole

PASS (exit=0)
  Output: 
  New AWS credentials 'cli-test-keys-with-role (4qd0L03f7UvgEeGf2XuphL)' added at user workspace

------------------------------------------------------------
TEST #8: Add credentials - keys mode with --generate-external-id
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-keys-ext-id -a AKIAIOSFODNN7EXAMPLE -s wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -r arn:aws:iam::123456789012:role/MyRole --generate-external-id

PASS (exit=0)
  Output: 
  New AWS credentials 'cli-test-keys-ext-id (4AICdJK68IlJtPAMGhPGw1)' added at user workspace

------------------------------------------------------------
TEST #9: Add credentials - role mode (External ID auto-generated)
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-role-mode --mode=role -r arn:aws:iam::123456789012:role/CrossAccountRole

PASS (exit=0)
  Output: 
  New AWS credentials 'cli-test-role-mode (66vspf9K9LHSFezx7QGK4Q)' added at user workspace

------------------------------------------------------------
TEST #10: List credentials - verify created
CMD:  /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials list

PASS (exit=0)
  Output: 
  Credentials at user workspace:

     ID                     | Provider | Name                    | Last activity                 
    ------------------------+----------+-------------------------+-------------------------------
     4AICdJK68IlJtPAMGhPGw1 | aws      | cli-test-keys-ext-id    | Thu, 26 Feb 2026 11:28:49 GMT 
     4VNWeM0iE0WFNIF9vINMEa | aws      | cli-test-keys-explicit  | Thu, 26 Feb 2026 11:28:44 GMT 
     4qd0L03f7UvgEeGf2XuphL | aws      | cli-test-keys-with-role | Thu, 26 Feb 2026 11:28:44 GMT 
     66vspf9K9LHSFezx7QGK4Q | aws      | cli-test-role-mode      | never                         
     6v46nMt7xDhj7DqSnnYNhu | aws      | cli-test-keys-implicit  | Thu, 26 Feb 2026 11:28:39 GMT 
     RJLmH4DIspH4UW48QH9wm  | aws      | test-keys               | Thu, 26 Feb 2026 11:22:09 GMT 

>>> Cleaning up test credentials...

  Credentials '6v46nMt7xDhj7DqSnnYNhu' deleted at user workspace


  Credentials '4VNWeM0iE0WFNIF9vINMEa' deleted at user workspace


  Credentials '4qd0L03f7UvgEeGf2XuphL' deleted at user workspace


  Credentials '4AICdJK68IlJtPAMGhPGw1' deleted at user workspace


  Credentials '66vspf9K9LHSFezx7QGK4Q' deleted at user workspace

Done.

============================================================
  Results: 10 passed, 0 failed, 10 total
============================================================

@munishchouhan munishchouhan requested a review from a team February 26, 2026 11:30
@ramonamela
Copy link
Contributor

Role mode requires --assume-role-arn, rejects --access-key/--secret-key, and auto-generates an External ID

Regarding this, we should only generate an External ID if --generate-external-id is there and this option should only be valid if --assume-role-arn, right?

@munishchouhan
Copy link
Member Author

Role mode requires --assume-role-arn, rejects --access-key/--secret-key, and auto-generates an External ID

Regarding this, we should only generate an External ID if --generate-external-id is there and this option should only be valid if --assume-role-arn, right?

  1. with mode=role, externalId is auto generated
  2. with mode=key, externalId is optional and also requires role arn

@munishchouhan
Copy link
Member Author

tested:

tower-cli % /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-keys-explicit --mode=keys \
        -a AKIAIOSFODNN7EXAMPLE -s wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --generate-external-id

 ERROR: A credentials with name 'cli-test-keys-explicit' already exists for user 'munish-chouhan
 
 tower-cli % /Users/munish.chouhan/testing_ground/tower-cli/build/native/nativeCompile/tw credentials add aws --name=cli-test-keys-explicit1 --mode=keys \
        -a AKIAIOSFODNN7EXAMPLE -s wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --generate-external-id --assume-role-arn arn:aws:iam::123456789012:role/MyRole

  New AWS credentials 'cli-test-keys-explicit1 (3oZRaOiMmgsTdBUrRhaVvu)' added at user workspace
```'

@ramonamela
Copy link
Contributor

Review findings

Overall the change looks good — clean validation, good test coverage, correct API integration. A few things to address:

1. AwsProvider.javaIllegalArgumentException leaks a stack trace to users

The validation errors at lines 83–101 throw IllegalArgumentException. The CLI's error handler (ResponseHelper.errorMessage) has no specific branch for this exception type, so it falls through to e.printStackTrace(err) (line 114 of ResponseHelper.java). Users will see a full Java stack trace instead of a clean error message. The tests pass because they assert stdErr.contains(...) which matches within the stack trace.

Switch to TowerRuntimeException (or another type already handled cleanly) so the user only sees the message.

2. AwsProvider.java:33-34--mode should use the enum type directly

The mode field is typed as String and parsed manually with a switch in getMode(). Picocli natively supports enum types, and Tower.java:111 already enables setCaseInsensitiveEnumValuesAllowed(true). There are several existing examples in the codebase:

  • AwsBatchForgePlatform.java:45-46TypeEnum provisioningModel for --provisioning-model
  • LaunchCmd.java:85-86WorkflowStatus wait for --wait (even uses ${COMPLETION-CANDIDATES})
  • Tower.java:91-92OutputType output for --output

Declaring the field as AwsCredentialsMode mode would eliminate the manual switch, the getMode() method, and the custom "invalid mode" validation — picocli handles all of that automatically, including a clean error message for invalid values.

3. gradle/libs.versions.tomljacksonDatabindNullable is declared but unused

The entry jacksonDatabindNullable (version 0.2.8) was added to the version catalog but is never referenced in build.gradle. The library is already pulled transitively by the tower-java-sdk (both old and new versions — reflect-config.json already has 24–25 references to org.openapitools.jackson.nullable.JsonNullable).

Either:

  • Remove the catalog entry if it's not needed as a direct dependency (the transitive one works fine), or
  • Add implementation(libs.jacksonDatabindNullable) to build.gradle if the intent was to pin the version explicitly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants