davsclaus commented on code in PR #26674:
URL: https://github.com/apache/camel/pull/26674#discussion_r4065198881


##########
components/camel-aws/camel-aws-common/src/main/java/org/apache/camel/component/aws/common/AwsClientBuilderUtil.java:
##########
@@ -299,4 +307,16 @@ private static AwsCredentialsProvider 
resolveCredentialsProvider(AwsCommonConfig
         LOG.trace("No explicit credentials configured, using SDK default 
chain");
         return null;
     }
+
+    private static void 
warnOnConflictingCredentialsOptions(AwsCommonConfiguration config) {

Review Comment:
   Minor: `profileCredentialsName` is also silently ignored under auto-detect 
(`ProfileCredentialsProvider.create()` uses `AWS_PROFILE` / the default 
profile), but it isn't part of this check. Worth adding 
`ObjectHelper.isNotEmpty(config.getProfileCredentialsName())` so the warning 
fires for that case too.
   



##########
components/camel-aws/camel-aws-common/src/main/java/org/apache/camel/component/aws/common/AwsRuntimeCredentialsResolver.java:
##########
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.aws.common;
+
+import java.io.File;
+
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain;
+import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import 
software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
+import 
software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
+
+/**
+ * Detects which AWS credentials source applies to the current runtime - JVM 
system properties, environment variables,
+ * web identity / IRSA, a shared profile, or ECS / EKS Pod Identity container 
credentials - selects the matching
+ * provider, and reports the chosen source at INFO.
+ * <p>
+ * This is an opt-in enhancement over the SDK {@link 
DefaultCredentialsProvider} whose purpose is observability. The SDK
+ * chain already resolves credentials in the same order used here, so the 
selected source never differs from the SDK;
+ * this class simply makes the resolved source visible in the logs and returns 
it as a targeted provider. When no source
+ * is recognised it returns {@code null} so the caller falls back to the SDK 
default chain (which also covers EC2
+ * instance metadata).
+ * </p>
+ * <p>
+ * The detected provider is returned as the head of a chain whose tail is the 
full {@link DefaultCredentialsProvider},
+ * so a detected-but-unusable source (for example a profile without resolvable 
credentials) still falls back to the SDK
+ * default chain rather than failing.
+ * </p>
+ *
+ * @since 4.23
+ */
+public final class AwsRuntimeCredentialsResolver {
+
+    static final String ENV_ACCESS_KEY = "AWS_ACCESS_KEY_ID";
+    static final String ENV_SECRET_KEY = "AWS_SECRET_ACCESS_KEY";
+    static final String ENV_WEB_IDENTITY_TOKEN_FILE = 
"AWS_WEB_IDENTITY_TOKEN_FILE";
+    static final String ENV_ROLE_ARN = "AWS_ROLE_ARN";
+    static final String ENV_CONTAINER_RELATIVE_URI = 
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
+    static final String ENV_CONTAINER_FULL_URI = 
"AWS_CONTAINER_CREDENTIALS_FULL_URI";
+    static final String ENV_PROFILE = "AWS_PROFILE";
+
+    static final String SYS_ACCESS_KEY = "aws.accessKeyId";
+    static final String SYS_SECRET_KEY = "aws.secretAccessKey";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(AwsRuntimeCredentialsResolver.class);
+
+    private AwsRuntimeCredentialsResolver() {
+    }
+
+    /**
+     * The credentials source detected for the current runtime.
+     */
+    public enum Source {
+        SYSTEM_PROPERTY("JVM system properties 
(aws.accessKeyId/aws.secretAccessKey)"),
+        ENVIRONMENT("environment variables 
(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY)"),
+        WEB_IDENTITY("web identity token / IRSA 
(AWS_WEB_IDENTITY_TOKEN_FILE)"),
+        PROFILE("shared profile (AWS_PROFILE or ~/.aws/credentials)"),
+        CONTAINER("container credentials (ECS task role / EKS Pod Identity)"),
+        UNKNOWN("no recognised runtime");
+
+        private final String description;
+
+        Source(String description) {
+            this.description = description;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+    }
+
+    /**
+     * Detect the current runtime and return the matching credentials 
provider, or {@code null} when no runtime can be
+     * recognised (so the caller falls back to the SDK default credentials 
provider chain).
+     *
+     * @return the resolved credentials provider, or {@code null} to use the 
SDK default chain
+     */
+    public static AwsCredentialsProvider resolve() {
+        return resolve(RuntimeEnvironment.SYSTEM);
+    }
+
+    static AwsCredentialsProvider resolve(RuntimeEnvironment environment) {
+        Source source = detect(environment);

Review Comment:
   **Design question (not a defect in the code as written):** the INFO line 
names a *predicted* source, not the *observed* one.
   
   `detect()` decides from env vars / file presence, and `resolve()` returns 
`chain(providerFor(source), DefaultCredentialsProvider)`. When the predicted 
head is unusable — the class Javadoc gives the example itself: 
`~/.aws/credentials` exists but the default profile has no keys — the log says 
`detected shared profile`, `ProfileCredentialsProvider` throws, the chain falls 
through to the full default provider, and the credentials actually come from 
the container or IMDS. So the one thing the option promises ("which source did 
my credentials come from?") is wrong in precisely the situation a user would 
enable it to diagnose. The same applies to `WEB_IDENTITY` when `sts` is absent.
   
   The SDK already exposes the *observed* source, with no new option:
   
   - `AwsCredentialsProviderChain` logs `Loading credentials from <provider>` 
at DEBUG (checked in `auth-2.55.0.jar`, which `parent/pom.xml` pins now).
   - Every resolved `AwsCredentials` carries `Identity.providerName()` 
(`identity-spi-2.55.0`), naming the provider that actually produced it.
   
   So an alternative that is never wrong and has nothing to keep in sync with 
the SDK's chain order: keep the SDK default chain and log 
`credentials.providerName()` at INFO after the first successful resolve (or 
simply point users at the DEBUG logger). That's a bigger change of direction 
than a review fix, so I'm raising it as a question for you and @davsclaus 
rather than requesting changes. If the answer is "keep the option", it would at 
least be worth wording the log line as "will try X first" so it doesn't claim 
more than it knows.
   



##########
components/camel-aws/camel-aws-common/src/test/java/org/apache/camel/component/aws/common/AwsRuntimeCredentialsResolverTest.java:
##########
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.aws.common;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import 
org.apache.camel.component.aws.common.AwsRuntimeCredentialsResolver.RuntimeEnvironment;
+import 
org.apache.camel.component.aws.common.AwsRuntimeCredentialsResolver.Source;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AwsRuntimeCredentialsResolverTest {
+
+    @Test
+    void detectsSystemProperties() {
+        FakeEnvironment env = new FakeEnvironment()
+                .prop(AwsRuntimeCredentialsResolver.SYS_ACCESS_KEY, "AKIA")
+                .prop(AwsRuntimeCredentialsResolver.SYS_SECRET_KEY, "secret");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.SYSTEM_PROPERTY);
+    }
+
+    @Test
+    void detectsEnvironmentVariables() {
+        FakeEnvironment env = new FakeEnvironment()
+                .env(AwsRuntimeCredentialsResolver.ENV_ACCESS_KEY, "AKIA")
+                .env(AwsRuntimeCredentialsResolver.ENV_SECRET_KEY, "secret");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.ENVIRONMENT);
+    }
+
+    @Test
+    void detectsWebIdentity() {
+        FakeEnvironment env = new FakeEnvironment()
+                
.env(AwsRuntimeCredentialsResolver.ENV_WEB_IDENTITY_TOKEN_FILE, 
"/var/run/secrets/token")
+                .env(AwsRuntimeCredentialsResolver.ENV_ROLE_ARN, 
"arn:aws:iam::123456789012:role/app");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.WEB_IDENTITY);
+    }
+
+    @Test
+    void detectsProfileFromEnv() {
+        FakeEnvironment env = new 
FakeEnvironment().env(AwsRuntimeCredentialsResolver.ENV_PROFILE, "dev");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.PROFILE);
+    }
+
+    @Test
+    void detectsProfileFromCredentialsFile() {
+        FakeEnvironment env = new FakeEnvironment()
+                .home("/home/tester")
+                .file("/home/tester/.aws/credentials");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.PROFILE);
+    }
+
+    @Test
+    void bareConfigFileIsNotAProfileSignal() {
+        // ~/.aws/config often exists with only a region set - it must not be 
read as "profile credentials detected".
+        FakeEnvironment env = new FakeEnvironment()
+                .home("/home/tester")
+                .file("/home/tester/.aws/config");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.UNKNOWN);
+    }
+
+    @Test
+    void detectsContainer() {
+        FakeEnvironment env = new FakeEnvironment()
+                .env(AwsRuntimeCredentialsResolver.ENV_CONTAINER_FULL_URI, 
"http://169.254.170.23/v1/credentials";);
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.CONTAINER);
+    }
+
+    @Test
+    void unknownWhenNothingDetected() {
+        assertThat(AwsRuntimeCredentialsResolver.detect(new 
FakeEnvironment())).isEqualTo(Source.UNKNOWN);
+    }
+
+    @Test
+    void mirrorsSdkOrderProfileBeforeContainer() {
+        // The SDK DefaultCredentialsProvider probes the profile provider 
before the container provider - match it.
+        FakeEnvironment env = new FakeEnvironment()
+                .env(AwsRuntimeCredentialsResolver.ENV_PROFILE, "dev")
+                .env(AwsRuntimeCredentialsResolver.ENV_CONTAINER_FULL_URI, 
"http://169.254.170.23/v1/credentials";);
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.PROFILE);
+    }
+
+    @Test
+    void environmentTakesPrecedenceOverProfile() {
+        FakeEnvironment env = new FakeEnvironment()
+                .env(AwsRuntimeCredentialsResolver.ENV_ACCESS_KEY, "AKIA")
+                .env(AwsRuntimeCredentialsResolver.ENV_SECRET_KEY, "secret")
+                .env(AwsRuntimeCredentialsResolver.ENV_PROFILE, "dev");
+        
assertThat(AwsRuntimeCredentialsResolver.detect(env)).isEqualTo(Source.ENVIRONMENT);
+    }
+
+    @Test
+    void resolveReturnsChainForConcreteSource() {
+        FakeEnvironment env = new 
FakeEnvironment().env(AwsRuntimeCredentialsResolver.ENV_PROFILE, "dev");
+        AwsCredentialsProvider provider = 
AwsRuntimeCredentialsResolver.resolve(env);
+        assertThat(provider).isInstanceOf(AwsCredentialsProviderChain.class);
+    }
+
+    @Test
+    void resolveDelegatesToDefaultForWebIdentity() {
+        FakeEnvironment env = new FakeEnvironment()
+                
.env(AwsRuntimeCredentialsResolver.ENV_WEB_IDENTITY_TOKEN_FILE, 
"/var/run/secrets/token")
+                .env(AwsRuntimeCredentialsResolver.ENV_ROLE_ARN, 
"arn:aws:iam::123456789012:role/app");
+        AwsCredentialsProvider provider = 
AwsRuntimeCredentialsResolver.resolve(env);
+        assertThat(provider).isInstanceOf(DefaultCredentialsProvider.class);
+    }
+
+    @Test
+    void resolveReturnsNullWhenUnknown() {

Review Comment:
   Minor: the resolver is well covered, but nothing exercises the 
`AwsClientBuilderUtil` side — that `useAutoDetectCredentialsProvider=true` 
actually takes precedence over `useDefaultCredentialsProvider` / static keys, 
and that `warnOnConflictingCredentialsOptions` fires. A small test with a stub 
`AwsCommonConfiguration` would close that gap.
   



-- 
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]

Reply via email to