-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Enable Custom Proxy in WorkloadIdentityCredential #47041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,9 @@ | |
import com.azure.core.util.CoreUtils; | ||
import com.azure.core.util.logging.ClientLogger; | ||
import com.azure.identity.implementation.IdentityClientOptions; | ||
import com.azure.identity.implementation.customtokenproxy.CustomTokenProxyConfiguration; | ||
import com.azure.identity.implementation.customtokenproxy.CustomTokenProxyHttpClient; | ||
import com.azure.identity.implementation.customtokenproxy.ProxyConfig; | ||
import com.azure.identity.implementation.util.LoggingUtil; | ||
import com.azure.identity.implementation.util.ValidationUtil; | ||
import reactor.core.publisher.Mono; | ||
|
@@ -89,6 +92,15 @@ public class WorkloadIdentityCredential implements TokenCredential { | |
ClientAssertionCredential tempClientAssertionCredential = null; | ||
String tempClientId = null; | ||
|
||
if (identityClientOptions.isKubernetesTokenProxyEnabled()) { | ||
if (!CustomTokenProxyConfiguration.isConfigured(configuration)) { | ||
throw LOGGER.logExceptionAsError( | ||
new IllegalArgumentException("Kubernetes token proxy is enabled but not configured.")); | ||
} | ||
ProxyConfig proxyConfig = CustomTokenProxyConfiguration.parseAndValidate(configuration); | ||
identityClientOptions.setHttpClient(new CustomTokenProxyHttpClient(proxyConfig)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this throw when the underlying |
||
} | ||
|
||
if (!(CoreUtils.isNullOrEmpty(tenantIdInput) | ||
|| CoreUtils.isNullOrEmpty(federatedTokenFilePathInput) | ||
|| CoreUtils.isNullOrEmpty(clientIdInput) | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,118 @@ | ||||||||||||
package com.azure.identity.implementation.customtokenproxy; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
import com.azure.core.util.logging.ClientLogger; | ||||||||||||
|
||||||||||||
import java.net.URI; | ||||||||||||
import java.net.URL; | ||||||||||||
import java.nio.charset.StandardCharsets; | ||||||||||||
import java.net.URISyntaxException; | ||||||||||||
|
||||||||||||
import com.azure.core.util.Configuration; | ||||||||||||
import com.azure.core.util.CoreUtils; | ||||||||||||
|
||||||||||||
public class CustomTokenProxyConfiguration { | ||||||||||||
|
||||||||||||
private static final ClientLogger LOGGER = new ClientLogger(CustomTokenProxyConfiguration.class); | ||||||||||||
|
||||||||||||
public static final String AZURE_KUBERNETES_TOKEN_PROXY = "AZURE_KUBERNETES_TOKEN_PROXY"; | ||||||||||||
public static final String AZURE_KUBERNETES_CA_FILE = "AZURE_KUBERNETES_CA_FILE"; | ||||||||||||
public static final String AZURE_KUBERNETES_CA_DATA = "AZURE_KUBERNETES_CA_DATA"; | ||||||||||||
public static final String AZURE_KUBERNETES_SNI_NAME = "AZURE_KUBERNETES_SNI_NAME"; | ||||||||||||
|
||||||||||||
private CustomTokenProxyConfiguration() { | ||||||||||||
} | ||||||||||||
|
||||||||||||
public static boolean isConfigured(Configuration configuration) { | ||||||||||||
String tokenProxyUrl = configuration.get(AZURE_KUBERNETES_TOKEN_PROXY); | ||||||||||||
return !CoreUtils.isNullOrEmpty(tokenProxyUrl); | ||||||||||||
} | ||||||||||||
|
||||||||||||
public static ProxyConfig parseAndValidate(Configuration configuration) { | ||||||||||||
String tokenProxyUrl = configuration.get(AZURE_KUBERNETES_TOKEN_PROXY); | ||||||||||||
String caFile = configuration.get(AZURE_KUBERNETES_CA_FILE); | ||||||||||||
String caData = configuration.get(AZURE_KUBERNETES_CA_DATA); | ||||||||||||
String sniName = configuration.get(AZURE_KUBERNETES_SNI_NAME); | ||||||||||||
|
||||||||||||
if (CoreUtils.isNullOrEmpty(tokenProxyUrl)) { | ||||||||||||
if (!CoreUtils.isNullOrEmpty(sniName) | ||||||||||||
|| !CoreUtils.isNullOrEmpty(caFile) | ||||||||||||
|| !CoreUtils.isNullOrEmpty(caData)) { | ||||||||||||
throw LOGGER.logExceptionAsError(new IllegalStateException( | ||||||||||||
"AZURE_KUBERNETES_TOKEN_PROXY is not set but other custom endpoint-related environment variables are present")); | ||||||||||||
} | ||||||||||||
return null; | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (!CoreUtils.isNullOrEmpty(caFile) && !CoreUtils.isNullOrEmpty(caData)) { | ||||||||||||
throw LOGGER.logExceptionAsError(new IllegalStateException( | ||||||||||||
"Only one of AZURE_KUBERNETES_CA_FILE or AZURE_KUBERNETES_CA_DATA can be set.")); | ||||||||||||
} | ||||||||||||
|
||||||||||||
URL proxyUrl = validateProxyUrl(tokenProxyUrl); | ||||||||||||
|
||||||||||||
byte[] caCertBytes = null; | ||||||||||||
if (!CoreUtils.isNullOrEmpty(caData)) { | ||||||||||||
try { | ||||||||||||
caCertBytes = caData.getBytes(StandardCharsets.UTF_8); | ||||||||||||
} catch (Exception e) { | ||||||||||||
throw LOGGER.logExceptionAsError(new IllegalArgumentException( | ||||||||||||
"Failed to decode CA certificate data from AZURE_KUBERNETES_CA_DATA", e)); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
return new ProxyConfig(proxyUrl, sniName, caFile, caCertBytes); | ||||||||||||
} | ||||||||||||
|
||||||||||||
private static URL validateProxyUrl(String endpoint) { | ||||||||||||
if (CoreUtils.isNullOrEmpty(endpoint)) { | ||||||||||||
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Proxy endpoint cannot be null or empty")); | ||||||||||||
} | ||||||||||||
|
||||||||||||
try { | ||||||||||||
URI tokenProxy = new URI(endpoint); | ||||||||||||
|
||||||||||||
if (!"https".equals(tokenProxy.getScheme())) { | ||||||||||||
throw LOGGER.logExceptionAsError(new IllegalArgumentException( | ||||||||||||
"Custom token endpoint must use https scheme, got: " + tokenProxy.getScheme())); | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (tokenProxy.getRawUserInfo() != null) { | ||||||||||||
throw LOGGER.logExceptionAsError( | ||||||||||||
new IllegalArgumentException("Custom token endpoint URL must not contain user info: " + endpoint)); | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (tokenProxy.getRawQuery() != null) { | ||||||||||||
throw LOGGER.logExceptionAsError( | ||||||||||||
new IllegalArgumentException("Custom token endpoint URL must not contain a query: " + endpoint)); | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (tokenProxy.getRawFragment() != null) { | ||||||||||||
throw LOGGER.logExceptionAsError( | ||||||||||||
new IllegalArgumentException("Custom token endpoint URL must not contain a fragment: " + endpoint)); | ||||||||||||
} | ||||||||||||
|
||||||||||||
if (tokenProxy.getRawPath() == null || tokenProxy.getRawPath().isEmpty()) { | ||||||||||||
tokenProxy = new URI(tokenProxy.getScheme(), null, tokenProxy.getHost(), tokenProxy.getPort(), "/", | ||||||||||||
null, null); | ||||||||||||
} | ||||||||||||
|
||||||||||||
return tokenProxy.toURL(); | ||||||||||||
|
||||||||||||
} catch (URISyntaxException | IllegalArgumentException e) { | ||||||||||||
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Failed to normalize proxy URL path", e)); | ||||||||||||
} catch (Exception e) { | ||||||||||||
throw new RuntimeException("Unexpected error while validating proxy URL: " + endpoint, e); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
// public static String getTokenProxyUrl() { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👀 |
||||||||||||
// String tokenProxyUrl = System.getenv(AZURE_KUBERNETES_TOKEN_PROXY); | ||||||||||||
// if (tokenProxyUrl == null || tokenProxyUrl.isEmpty()) { | ||||||||||||
// throw LOGGER.logExceptionAsError(new IllegalStateException( | ||||||||||||
// String.format("Environment variable '%s' is not set or is empty. It must be set to the URL of the" | ||||||||||||
// + " token proxy.", AZURE_KUBERNETES_TOKEN_PROXY))); | ||||||||||||
// } | ||||||||||||
// return tokenProxyUrl; | ||||||||||||
// } | ||||||||||||
|
||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absent configuration isn't an error. If the feature is enabled but none of the relevant environment variables are set, the credential should behave as if the feature weren't enabled i.e., send all requests to Entra. We should throw only when the feature is enabled, environment variable configuration is present, and that configuration is invalid.