-
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 1 commit
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 |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package com.azure.identity.implementation.customtokenproxy; | ||
|
||
import com.azure.core.util.logging.ClientLogger; | ||
import com.azure.identity.implementation.WorkloadIdentityTokenProxyPolicy; | ||
|
||
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(WorkloadIdentityTokenProxyPolicy.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")); | ||
} | ||
throw LOGGER.logExceptionAsError(new IllegalStateException( | ||
"AZURE_KUBERNETES_TOKEN_PROXY must be set to enable custom token proxy.")); | ||
} | ||
|
||
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() { | ||
// 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; | ||
// } | ||
|
||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,218 @@ | ||||||||||||||
package com.azure.identity.implementation.customtokenproxy; | ||||||||||||||
|
||||||||||||||
import java.io.ByteArrayInputStream; | ||||||||||||||
import java.io.IOException; | ||||||||||||||
import java.io.InputStream; | ||||||||||||||
import java.lang.reflect.MalformedParametersException; | ||||||||||||||
import java.lang.reflect.Proxy; | ||||||||||||||
|
import java.lang.reflect.Proxy; |
Copilot uses AI. Check for mistakes.
Outdated
Copilot
AI
Oct 19, 2025
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.
setDoOutput(true) is applied unconditionally; restrict this to methods that actually send a body (e.g., POST/PUT/PATCH) to avoid unintended semantics on GET/DELETE requests.
connection.setDoOutput(true); | |
// Only set doOutput for methods that support a body | |
String method = request.getHttpMethod().toString(); | |
if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) { | |
connection.setDoOutput(true); | |
} |
Copilot uses AI. Check for mistakes.
Outdated
Copilot
AI
Oct 19, 2025
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.
The output stream is not closed or flushed, leading to a potential resource leak; wrap the write in a try-with-resources: try (OutputStream os = connection.getOutputStream()) { os.write(bytes); }.
connection.getOutputStream().write(bytes); | |
try (java.io.OutputStream os = connection.getOutputStream()) { | |
os.write(bytes); | |
} |
Copilot uses AI. Check for mistakes.
Outdated
Copilot
AI
Oct 19, 2025
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.
MalformedParametersException (from java.lang.reflect) is unrelated here and never thrown; remove the import and the throws clause (or replace with MalformedURLException if actually needed) to prevent confusion and inaccurate exception signaling.
Copilot uses AI. Check for mistakes.
Outdated
Copilot
AI
Oct 19, 2025
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.
Corrected wording from 'provide' to 'provided'.
// If no CA override provide, use default | |
// If no CA override provided, use default |
Copilot uses AI. Check for mistakes.
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.
MalformedParametersException (from java.lang.reflect) is unrelated here and never thrown; remove the import and the throws clause (or replace with MalformedURLException if actually needed) to prevent confusion and inaccurate exception signaling.
Copilot uses AI. Check for mistakes.