amaechler commented on code in PR #20540:
URL: https://github.com/apache/druid/pull/20540#discussion_r4285845758


##########
extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java:
##########
@@ -411,6 +412,12 @@ public static ServerSideEncryptingAmazonS3.Builder builder(
 
     // Apply assume role if configured
     if (!Strings.isNullOrEmpty(assumeRoleArn)) {
+      credentialsProvider = maybeAssumeIntermediateRole(
+          credentialsProvider,
+          intermediateRoleConfig,
+          awsClientConfig
+      );

Review Comment:
   Suggestion for the spec-credentials thread on line 415: take the 
intermediate hop only when the spec doesn't supply its own keys.
   
   ```suggestion
         if (s3InputSourceConfig == null || 
!s3InputSourceConfig.isCredentialsConfigured()) {
           credentialsProvider = maybeAssumeIntermediateRole(
               credentialsProvider,
               intermediateRoleConfig,
               awsClientConfig
           );
         }
   ```



##########
extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java:
##########
@@ -461,6 +469,30 @@ private static StaticCredentialsProvider 
createStaticCredentialsProvider(S3Input
     }
   }
 
+  /**
+   * Assumes an intermediate role if one is configured.
+   */
+  public static AwsCredentialsProvider maybeAssumeIntermediateRole(

Review Comment:
   nit: only called from `builder()`, so it can be `private`.
   
   ```suggestion
     private static AwsCredentialsProvider maybeAssumeIntermediateRole(
   ```



##########
extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java:
##########
@@ -411,6 +412,13 @@ public static ServerSideEncryptingAmazonS3.Builder builder(
 
     // Apply assume role if configured
     if (!Strings.isNullOrEmpty(assumeRoleArn)) {
+      credentialsProvider = maybeAssumeIntermediateRole(

Review Comment:
   My snippet only gates the intermediate hop, not `assumeRoleArn`. A spec with 
its own keys would keep using them to assume `assumeRoleArn` directly, the same 
as on master, so it's the variant without a behaviour change.
   
   On why only the intermediate role fails: for a spec that works today with 
its own keys, the role at `assumeRoleArn` already lets the principal behind 
those keys assume it. The intermediate role is set up to trust the cluster's 
credentials. On this branch the spec's keys sign the intermediate AssumeRole 
call (its STS client is backed by a `StaticCredentialsProvider` holding them), 
and 
[AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
 only succeeds for principals the role's trust policy allows. So an existing 
spec with its own keys starts failing as soon as an operator sets 
`druid.s3.intermediateAssumeRoleArn`, unless the intermediate role's trust 
policy also admits those keys.
   
   I left the change as a suggestion on lines 415-419, with a test and a docs 
line. `input-sources.md` already describes this behaviour, since the new 
sentence there sits under "If `accessKeyId` and `secretAccessKey` are not 
given".



##########
extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java:
##########
@@ -461,6 +469,31 @@ private static StaticCredentialsProvider 
createStaticCredentialsProvider(S3Input
     }
   }
 
+  /**
+   * Assumes an intermediate role if one is configured.
+   */
+  public static AwsCredentialsProvider maybeAssumeIntermediateRole(
+      AwsCredentialsProvider baseCredentialsProvider,
+      @Nullable S3IntermediateRoleConfig intermediateRoleConfig,
+      @Nullable AWSEndpointConfig awsEndpointConfig,
+      @Nullable AWSClientConfig awsClientConfig
+  )
+  {
+    final String intermediateAssumeRoleArn =
+        intermediateRoleConfig == null ? null : 
intermediateRoleConfig.getIntermediateAssumeRoleArn();
+    if (Strings.isNullOrEmpty(intermediateAssumeRoleArn)) {
+      return baseCredentialsProvider;
+    }
+
+    return createAssumeRoleCredentialsProvider(
+        intermediateAssumeRoleArn,
+        null,
+        awsEndpointConfig,

Review Comment:
   Both comments are right about different cases, so neither `null` nor the 
spec's `endpointConfig` works everywhere. The intermediate role belongs to the 
operator, so I think that hop should use the process's own region: 
`druid.s3.endpoint.signingRegion` when set, otherwise the SDK default chain.
   
   - Not the spec's region: per the [AWS 
docs](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html#sts-regions-activate-deactivate),
 an AssumeRole call to the STS endpoint of a manually enabled (opt-in) Region 
needs that Region enabled in the account that contains the role. With the 
spec's region, an ingestion from a bucket in an opt-in Region that the 
intermediate role's account hasn't enabled would fail at the first hop.
   - Not plain `null`: I reproduced the case from the other thread on this 
branch. With no `AWS_REGION`, `aws.region`, profile or IMDS region and 
`druid.s3.endpoint.signingRegion` set, both an `S3InputSource` whose spec sets 
`endpointConfig.signingRegion` and an `S3ExportStorageProvider` work without 
`intermediateAssumeRoleArn` and throw `SdkClientException: Unable to load 
region from any of the providers in the chain` once it is set.
   
   This is what I tried locally, on top of the suggestion on lines 415-419. 
`S3ExportStorageProvider` already injects the process `AWSEndpointConfig`, and 
`S3InputSource` gets it from the injected `Builder`, the same way as 
`getIntermediateRoleConfig()`:
   
   ```diff
   diff --git 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/data/input/s3/S3InputSource.java
 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/data/input/s3/S3InputSource.java
   index fc996f9853..9164c9bad2 100644
   --- 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/data/input/s3/S3InputSource.java
   +++ 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/data/input/s3/S3InputSource.java
   @@ -131,5 +131,6 @@ public class S3InputSource extends CloudObjectInputSource
              s3InputSourceConfig,
              null,
   -          s3ClientBuilder.getIntermediateRoleConfig()
   +          s3ClientBuilder.getIntermediateRoleConfig(),
   +          s3ClientBuilder.getIntermediateRoleEndpointConfig()
          ).build();
        });
   diff --git 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/S3StorageDruidModule.java
 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/S3StorageDruidModule.java
   index 47352a3e6d..8ada5b5016 100644
   --- 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/S3StorageDruidModule.java
   +++ 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/S3StorageDruidModule.java
   @@ -153,5 +153,6 @@ public class S3StorageDruidModule implements DruidModule
            null,
            null,
   -        intermediateRoleConfig
   +        intermediateRoleConfig,
   +        endpointConfig
        );
      }
   diff --git 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java
 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java
   index 6cdcab631c..bf5da9c2c2 100644
   --- 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java
   +++ 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3.java
   @@ -324,5 +324,6 @@ public class ServerSideEncryptingAmazonS3
          @Nullable S3InputSourceConfig s3InputSourceConfig,
          @Nullable S3ExportStorageProvider s3ExportStorageProvider,
   -      @Nullable S3IntermediateRoleConfig intermediateRoleConfig
   +      @Nullable S3IntermediateRoleConfig intermediateRoleConfig,
   +      @Nullable AWSEndpointConfig intermediateRoleEndpointConfig
      )
      {
   @@ -417,4 +418,5 @@ public class ServerSideEncryptingAmazonS3
                credentialsProvider,
                intermediateRoleConfig,
   +            intermediateRoleEndpointConfig,
                awsClientConfig
            );
   @@ -438,5 +440,6 @@ public class ServerSideEncryptingAmazonS3
                                           
.setS3AsyncClientSupplier(asyncClientBuilder::build)
                                           .setS3StorageConfig(s3StorageConfig)
   -                                       
.setIntermediateRoleConfig(intermediateRoleConfig);
   +                                       
.setIntermediateRoleConfig(intermediateRoleConfig)
   +                                       
.setIntermediateRoleEndpointConfig(intermediateRoleEndpointConfig);
      }
    
   @@ -478,4 +481,5 @@ public class ServerSideEncryptingAmazonS3
          AwsCredentialsProvider baseCredentialsProvider,
          @Nullable S3IntermediateRoleConfig intermediateRoleConfig,
   +      @Nullable AWSEndpointConfig intermediateRoleEndpointConfig,
          @Nullable AWSClientConfig awsClientConfig
      )
   @@ -490,5 +494,5 @@ public class ServerSideEncryptingAmazonS3
            intermediateAssumeRoleArn,
            null,
   -        null,
   +        intermediateRoleEndpointConfig,
            baseCredentialsProvider,
            awsClientConfig
   @@ -537,4 +541,6 @@ public class ServerSideEncryptingAmazonS3
        @Nullable
        private S3IntermediateRoleConfig intermediateRoleConfig;
   +    @Nullable
   +    private AWSEndpointConfig intermediateRoleEndpointConfig;
    
        public Builder setS3ClientSupplier(Supplier<S3Client> s3ClientSupplier)
   @@ -573,4 +579,16 @@ public class ServerSideEncryptingAmazonS3
        }
    
   +    public Builder setIntermediateRoleEndpointConfig(@Nullable 
AWSEndpointConfig intermediateRoleEndpointConfig)
   +    {
   +      this.intermediateRoleEndpointConfig = intermediateRoleEndpointConfig;
   +      return this;
   +    }
   +
   +    @Nullable
   +    public AWSEndpointConfig getIntermediateRoleEndpointConfig()
   +    {
   +      return this.intermediateRoleEndpointConfig;
   +    }
   +
        /**
         * Builds a new {@link ServerSideEncryptingAmazonS3} instance.
   diff --git 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
   index 48f4194dd2..566d06c869 100644
   --- 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
   +++ 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
   @@ -119,5 +119,6 @@ public class S3ExportStorageProvider implements 
ExportStorageProvider
              null,
              this,
   -          intermediateRoleConfig
   +          intermediateRoleConfig,
   +          awsEndpointConfig
          ).build();
        });
   ```
   
   The three `builder(...)` calls in tests then need one more `null`, and 
`S3InputSourceTest` needs a `getIntermediateRoleEndpointConfig()` stub next to 
each `getIntermediateRoleConfig()` stub. Passing a second endpoint config 
(`eu-south-1`) in `testIntermediateRoleIsAssumedBeforeTheRoleTheSpecNames` pins 
both regions:
   
   ```java
         Assertions.assertEquals(Region.US_EAST_1, 
targetStsClient.serviceClientConfiguration().region());
         Assertions.assertEquals(Region.EU_SOUTH_1, ((StsClient) 
stsClientField.get(intermediate)).serviceClientConfiguration().region());
   ```
   
   With that, the S3 extension tests pass, and in the same no-region setup the 
intermediate hop goes to the `druid.s3.endpoint.signingRegion` Region while the 
target hop keeps the spec's. A process with no region anywhere except in a spec 
still fails, but the docs already require `aws.region` or `AWS_REGION`, and a 
spec without `signingRegion` fails on such a process today too.



##########
docs/development/extensions-core/s3.md:
##########
@@ -81,6 +81,24 @@ For more information, refer to the [Amazon Developer 
Guide](https://docs.aws.ama
 
 Alternatively, you can bypass this chain by specifying an access key and 
secret key using a [Properties 
Object](../../ingestion/input-sources.md#s3-input-source) inside your ingestion 
specification.
 
+Whichever credentials are resolved are then used to assume any `assumeRoleArn` 
named by an ingestion spec or export destination. Setting 
`druid.s3.intermediateAssumeRoleArn` inserts one role in between: the resolved 
credentials assume that role first, and the resulting credentials assume 
`assumeRoleArn`.

Review Comment:
   With the suggestion on `ServerSideEncryptingAmazonS3` lines 415-419, keys 
from the spec skip the intermediate role, so this paragraph could say so.
   
   ```suggestion
   Whichever credentials are resolved are then used to assume any 
`assumeRoleArn` named by an ingestion spec or export destination. Setting 
`druid.s3.intermediateAssumeRoleArn` inserts one role in between: the resolved 
credentials assume that role first, and the resulting credentials assume 
`assumeRoleArn`. Keys given in an ingestion spec's Properties Object skip the 
intermediate role and assume `assumeRoleArn` directly.
   ```



##########
extensions-core/s3-extensions/src/test/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3Test.java:
##########
@@ -282,13 +305,91 @@ public RetryStrategy getRetryStrategy()
         null,
         clientConfig,
         null,
+        null,
         null
     );
 
     Assertions.assertEquals(2, issued.size(), "one strategy per client, sync 
and async");
     Assertions.assertNotSame(issued.get(0), issued.get(1));
   }
 
+  @Test
+  public void testIntermediateRoleIsAssumedBeforeTheRoleTheSpecNames() throws 
Exception

Review Comment:
   I left a suggested test for this on line 317.



##########
extensions-core/s3-extensions/src/test/java/org/apache/druid/storage/s3/ServerSideEncryptingAmazonS3Test.java:
##########
@@ -282,13 +305,91 @@ public RetryStrategy getRetryStrategy()
         null,
         clientConfig,
         null,
+        null,
         null
     );
 
     Assertions.assertEquals(2, issued.size(), "one strategy per client, sync 
and async");
     Assertions.assertNotSame(issued.get(0), issued.get(1));
   }
 
+  @Test
+  public void testIntermediateRoleIsAssumedBeforeTheRoleTheSpecNames() throws 
Exception

Review Comment:
   A test for spec-supplied keys plus `assumeRoleArn`. It fails on the current 
branch (`expected: <false> but was: <true>`) and passes with the suggestion on 
`ServerSideEncryptingAmazonS3` lines 415-419.
   
   ```suggestion
     public void testSpecCredentialsAssumeTheRoleTheSpecNamesDirectly() throws 
Exception
     {
       final ServerSideEncryptingAmazonS3 s3 = 
ServerSideEncryptingAmazonS3.builder(
           AnonymousCredentialsProvider.create(),
           new S3StorageConfig(new NoopServerSideEncryption(), new 
S3TransferConfig()),
           null,
           REGION_CONFIG,
           new AWSClientConfig(),
           new S3InputSourceConfig(() -> "specAccessKey", () -> 
"specSecretKey", TARGET_ROLE_ARN, null, null),
           null,
           new S3IntermediateRoleConfig(INTERMEDIATE_ROLE_ARN)
       ).build();
   
       final StsAssumeRoleCredentialsProvider target = assumeRoleProvider(
           s3.getS3Client().serviceClientConfiguration().credentialsProvider(),
           "the S3 client should assume a role"
       );
       final Field stsClientField = 
StsCredentialsProvider.class.getDeclaredField("stsClient");
       stsClientField.setAccessible(true);
       final StsClient targetStsClient = (StsClient) stsClientField.get(target);
   
       try {
         Assertions.assertEquals(TARGET_ROLE_ARN, roleArnRequestedBy(target));
         Assertions.assertFalse(
             targetStsClient.serviceClientConfiguration().credentialsProvider() 
instanceof StsAssumeRoleCredentialsProvider,
             "the spec's own credentials assume the spec's role without the 
intermediate role"
         );
         Assertions.assertEquals(
             "specAccessKey",
             
targetStsClient.serviceClientConfiguration().credentialsProvider().resolveIdentity().join().accessKeyId()
         );
       }
       finally {
         target.close();
       }
     }
   
     @Test
     public void testIntermediateRoleIsAssumedBeforeTheRoleTheSpecNames() 
throws Exception
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to