Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.

The format is based on [Common Changelog](https://common-changelog.org/).

## [8.1.8 Build GH_POST_PR_COMMIT_RUN_ID] - 2026-08-12

### Fixed

- **EC2 instance metadata detection on IMDSv2-only hosts** (#1) — `PSPubServerService.isEC2Instance()` and `PSAmazonS3DeliveryHandler.isEC2Instance()` used to probe `http://169.254.169.254/latest/meta-data/` with a plain IMDSv1-style GET. On Amazon Linux 2023+ and other AMIs with `HttpTokens=required`, that probe fails and the host is treated as non-EC2, forcing operators to set static Access Key / Secret even when using an EC2 instance profile (with or without Assume Role). Both probe paths now delegate to a new `PSEc2InstanceMetadataClient` (`system/business/.../PSEc2InstanceMetadataClient.java`) that performs the IMDSv2 token flow (`PUT /latest/api/token` with `X-aws-ec2-metadata-token-ttl-seconds`, then `GET` with `X-aws-ec2-metadata-token`) and falls back to IMDSv1 only when the token endpoint is not available. The result is cached for the JVM lifetime, the first probe is gated by a `CountDownLatch` so concurrent callers wait for the same result instead of racing two IMDS probes, and `PSPubServerService` / `PSAmazonS3DeliveryHandler` are pure delegates (no second JVM cache layer).
- **S3 publish server save rejects empty AWS credentials on non-EC2 hosts** (#1) — `PSPubServerService.validatePropertiesByDriver` no longer rejects missing Access Key / Secret Key on save. Operators can leave them empty when relying on an EC2 instance profile (with or without Assume Role + ARN). The runtime `PSAmazonS3DeliveryHandler.getAmazonS3Client` now fails fast with an explicit `PSDeliveryException` when neither EC2 nor Assume Role is available and the static keys are blank, so the publish error is clear instead of a confusing auth failure on the first S3 API call. The UI (PercPublishMinuetView) downgrades the post-save success footer alert to a warning alert when S3 is selected and the Access Key / Secret Key are empty, so operators are still informed that the publish will fail at runtime on non-EC2 hosts.

### Added

- **IMDSv2-aware metadata client** — `system/business/src/com/percussion/rx/delivery/impl/PSEc2InstanceMetadataClient.java` with `MetadataTransport` indirection for testability, JVM-lifetime result cache, IMDSv2 → IMDSv1 fallback, `CountDownLatch` for concurrent first-call, and a `resetCache()` hook for tests / ops.
- **IMDSv2 unit tests** — `system/Testing/src/com/percussion/rx/delivery/impl/PSEc2InstanceMetadataClientTest.java` covering IMDSv2 success, IMDSv1 fallback when token PUT is rejected, non-EC2 connection-refused, IMDSv2 metadata GET failure with IMDSv1 also failing, result caching, concurrent first-call (single probe), and `resetCache()` re-probing.

### Notes

- AL2023+ defaults require `HttpPutResponseHopLimit >= 2` when running inside a container; operate-side documentation should mention this alongside the code fix.
- The optional follow-up to support DefaultCredentialsProvider / IRSA-style auth for non-EC2 hosts is not in scope here and remains a separate work item.
- Legacy `WebUI/war/views/PublishView.js` does not share the missing-S3-credentials warning; the warning is only surfaced in the Minuet (`PercPublishMinuetView.js`) save path.

## [8.1.7 Build GH_POST_PR_COMMIT_RUN_ID] - 2026-08-12

### Fixed
Expand Down
67 changes: 65 additions & 2 deletions WebUI/war/views/PercPublishMinuetView.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,14 +272,29 @@ function updateServerPropertiesCallback(status, result) {
response.result = {};
response.source = I18N.message("perc.ui.publish.title@Server Configuration");
if(result[1] == 'success') {
response.result.warning = false;
response.result.status = result[0].serverInfo.serverName + ' ' + I18N.message("perc.ui.publish.title@Updated Successfully");
var savedWarning = pendingS3MissingCredentialsWarning;
pendingS3MissingCredentialsWarning = null;
if (savedWarning) {
// Downgrade the success alert to a warning alert so the
// operator sees the "missing credentials" footer instead of
// a success footer. Reuses the existing processAlert path
// (templateResponseFooterAlert renders warning color when
// result.warning is truthy).
response.result.warning = result[0].serverInfo.serverName + ' '
+ I18N.message("perc.ui.publish.title@Updated Successfully")
+ ' \u2014 ' + savedWarning;
} else {
response.result.warning = false;
response.result.status = result[0].serverInfo.serverName + ' '
+ I18N.message("perc.ui.publish.title@Updated Successfully");
}
$('#percServerPropertiesModal').modal('toggle');
processAlert(response);
clearSelectedServer();
refreshServerList();
}
else{
pendingS3MissingCredentialsWarning = null;
responseText = JSON.parse(result.request.responseText);
response.result.warning = findVal(responseText, 'defaultMessage');
processAlert(response);
Expand Down Expand Up @@ -922,10 +937,58 @@ function deleteServerRequest() {
}

function updateServerProperties(siteName, serverName, serverProperties) {
// Non-modal warning when S3 is selected and the AWS credentials are empty.
// Returns the warning text (or null) so the callback can downgrade the
// success alert to a warning alert (existing Minuet footer-alert path).
pendingS3MissingCredentialsWarning = collectS3MissingCredentialsWarning(serverProperties);
startProcessRunningAlert();
$.PercPublisherService(false).createUpdateSiteServer(siteName, serverName, serverProperties, updateServerPropertiesCallback);
}

var pendingS3MissingCredentialsWarning = null;

function collectS3MissingCredentialsWarning(serverProperties) {
try {
var props = serverProperties && serverProperties.serverInfo
&& serverProperties.serverInfo.properties;
if (!props) {
return null;
}
var driver = null;
var accessKey = null;
var secretKey = null;
// The form posts keys in lowercase ("accesskey" / "securitykey") which
// match IPSPubServerDao.PUBLISH_AS3_ACCESSKEY_PROPERTY /
// PUBLISH_AS3_SECURITYKEY_PROPERTY on the server side.
for (var i = 0; i < props.length; i++) {
var p = props[i];
if (!p || !p.key) {
continue;
}
if (p.key === 'driver') {
driver = p.value;
} else if (p.key === 'accesskey') {
accessKey = p.value;
} else if (p.key === 'securitykey') {
secretKey = p.value;
}
}
if (driver !== 'AMAZONS3') {
return null;
}
var missingAccess = (accessKey === null || accessKey === undefined || accessKey === '');
var missingSecret = (secretKey === null || secretKey === undefined || secretKey === '');
if (missingAccess || missingSecret) {
return 'AWS Access Key and/or Secret Key are empty. Publishing will rely on '
+ 'an EC2 instance profile (with or without Assume Role). If the host is '
+ 'not on EC2, the publish will fail at runtime.';
}
} catch (e) {
// defensive: never block the save from the warning helper
}
return null;
}

function processServerPropertiesForm(eventData) {
formProps = {};
newServerProperties = {};
Expand Down
7 changes: 2 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2504,10 +2504,7 @@
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- <plugin>
<!-- <plugin>
<groupId>com.intsof</groupId>
<artifactId>ai-build-integrity-maven-plugin</artifactId>
<version>${ai-build-integrity.plugin.version}</version>
Expand Down Expand Up @@ -2551,7 +2548,7 @@
</plugins>
</pluginManagement>
<plugins>
<!-- <plugin>
<!-- <plugin>
<groupId>com.intsof</groupId>
<artifactId>ai-build-integrity-maven-plugin</artifactId>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import static com.percussion.share.service.exception.PSParameterValidationUtils.validateParameters;
import static com.percussion.utils.service.impl.PSSiteConfigUtils.removeServerEntry;
import static javax.ws.rs.client.ClientBuilder.newClient;
import static org.apache.commons.lang.StringUtils.EMPTY;
import static org.apache.commons.lang.StringUtils.equalsIgnoreCase;
import static org.apache.commons.lang.StringUtils.isBlank;
Expand All @@ -32,6 +31,7 @@
import com.percussion.pubserver.data.PSPublishServerInfo;
import com.percussion.pubserver.data.PSPublishServerProperty;
import com.percussion.rx.delivery.impl.PSBaseDeliveryHandler;
import com.percussion.rx.delivery.impl.PSEc2InstanceMetadataClient;
import com.percussion.rx.publisher.IPSRxPublisherService;
import com.percussion.security.PSEncryptionException;
import com.percussion.security.PSEncryptor;
Expand Down Expand Up @@ -89,11 +89,6 @@
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -132,7 +127,6 @@ public class PSPubServerService implements IPSPubServerService {
private final IPSPublisherService publisherService;
private final IPSContentChangeService contentChangeService;
private final IPSUtilityService utilityService;
private static Boolean isEC2Instance = null;
private IPSPublishingWs pubWs;
private SecureKeyRotationListener secureKeyRotationListener;

Expand Down Expand Up @@ -769,30 +763,10 @@ private PSPubServer createAmazonS3Server(IPSSite site) throws Exception {
}

public static Boolean isEC2Instance() {
if (isEC2Instance != null) {
return isEC2Instance;
}
try {
Client client = newClient();

WebTarget resource = client.target("http://169.254.169.254/latest/meta-data/");

Invocation.Builder request = resource.request();
request.accept(MediaType.APPLICATION_JSON);

Response response = request.get();

if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) {
isEC2Instance = Boolean.TRUE;
return true;
} else {
isEC2Instance = Boolean.FALSE;
}
} catch (Exception e) {
// means not an EC2 Server
isEC2Instance = Boolean.FALSE;
}
return isEC2Instance;
// Pure delegate to the IMDSv2-aware helper. The helper handles
// JVM-lifetime caching and concurrent first-call semantics, so this
// wrapper no longer carries its own (now unsafe) Boolean cache.
return PSEc2InstanceMetadataClient.isEC2Instance();
}

@Override
Expand Down Expand Up @@ -1830,19 +1804,18 @@ private void validatePropertiesByDriver(
String value = pubServerInfo.findProperty(property);
if (IPSPubServerDao.PUBLISH_AS3_BUCKET_PROPERTY.equals(property)) {
builder.rejectIfBlank(property, value).throwIfInvalid();
} else if (IPSPubServerDao.PUBLISH_AS3_ACCESSKEY_PROPERTY.equals(property)) {
if (!isEC2Instance()) {
builder.rejectIfBlank(property, value).throwIfInvalid();
}
} else if (IPSPubServerDao.PUBLISH_AS3_SECURITYKEY_PROPERTY.equals(property)) {
if (!isEC2Instance()) {
builder.rejectIfBlank(property, value).throwIfInvalid();
}
} else if (IPSPubServerDao.PUBLISH_AS3_ARN_ROLE.equals(property)) {
if (useAssumeRole) {
builder.rejectIfBlank(property, value).throwIfInvalid();
}
}
// Access Key / Secret Key are intentionally never required at save time.
Comment thread
vijaya-boddipudi marked this conversation as resolved.
// Operators may legitimately leave them blank when using an EC2 instance
// profile with or without Assume Role. On non-EC2 hosts with no Assume
// Role, the runtime path in PSAmazonS3DeliveryHandler.getAmazonS3Client
// // fails fast with an explicit PSDeliveryException so the publish error
// is clear. The UI (PercPublishMinuetView) also surfaces a non-modal
// warning when S3 keys are empty on save.
}
}

Expand Down
Loading
Loading