Skip to content

Commit 347dde8

Browse files
committed
OAK-12219: Upgrade Azure SDK V8 to V12 for oak-blob-azure - rework - address PR apache#2989 review comments
- cap configured presigned URI expiry to the 7-day Azure user delegation key lifetime under service-principal auth, with a warning - honor secondary-location failover in UtilsV12.getRetryOptions when no retry count is configured (use SDK default retries instead of dropping the secondary host) - expand @deprecated javadoc on AbstractAzureDataStoreService and AzureDataStoreService to explain the replacement - add tests for expiry capping (SP and non-SP) and secondary-location retry options
1 parent 5d4bd96 commit 347dde8

7 files changed

Lines changed: 103 additions & 7 deletions

File tree

oak-blob-cloud-azure/src/main/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AbstractAzureDataStoreService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
/**
3636
* Kept for binary compatibility with existing callers. Use {@link AzureDataStoreWrapper} instead.
3737
*
38-
* @deprecated
38+
* @deprecated superseded by {@link AzureDataStoreWrapper}, which replaces the dual-service
39+
* (v8/v12) OSGi architecture with a single FT-aware component that can toggle between SDK
40+
* versions at runtime without restart; retained only for binary compatibility.
3941
*/
4042
@Deprecated(since = "2.3", forRemoval = true)
4143
public abstract class AbstractAzureDataStoreService extends AbstractDataStoreService {

oak-blob-cloud-azure/src/main/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/AzureDataStoreService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
* {@link AzureDataStoreWrapper} owns the {@code AzureDataStore} PID. Use
2727
* {@link AzureDataStoreWrapper} instead.
2828
*
29-
* @deprecated
29+
* @deprecated superseded by {@link AzureDataStoreWrapper}, which replaces the dual-service
30+
* (v8/v12) OSGi architecture with a single FT-aware component that can toggle between SDK
31+
* versions at runtime without restart; retained only for binary compatibility.
3032
*/
3133
@Deprecated(since = "2.3", forRemoval = true)
3234
public class AzureDataStoreService extends AbstractAzureDataStoreService {

oak-blob-cloud-azure/src/main/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/v12/AzureBlobContainerProviderV12.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ class AzureBlobContainerProviderV12 {
7878
final AtomicReference<CachedDelegationKey> cachedDelegationKey = new AtomicReference<>();
7979

8080
// Request keys for the full 7-day window so they cover any SAS expiry we'd generate.
81-
private static final Duration DELEGATION_KEY_LIFETIME = Duration.ofDays(7);
81+
// Also the hard upper bound Azure allows for a user delegation key's lifetime — package-private
82+
// so callers can validate configured presigned-URI expiries against it (see AzureBlobStoreBackendV12).
83+
static final Duration DELEGATION_KEY_LIFETIME = Duration.ofDays(7);
8284
// Renew early enough to cover clock skew between this host and Azure.
8385
private static final Duration DELEGATION_KEY_RENEWAL_BUFFER = Duration.ofSeconds(60);
8486

@@ -286,7 +288,9 @@ public void close() {
286288
log.debug("AzureBlobContainerProviderV12 closed; cached Azure clients released");
287289
}
288290

289-
private boolean authenticateViaServicePrincipal() {
291+
// Package-private: AzureBlobStoreBackendV12 needs this to know whether presigned URIs will be
292+
// signed with a user delegation key (and are therefore bounded by DELEGATION_KEY_LIFETIME).
293+
boolean authenticateViaServicePrincipal() {
290294
return StringUtils.isBlank(azureConnectionString) &&
291295
StringUtils.isNoneBlank(accountName, tenantId, clientId, clientSecret);
292296
}

oak-blob-cloud-azure/src/main/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/v12/AzureBlobStoreBackendV12.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,11 @@ private void initContainerConnection() throws DataStoreException {
287287
private void initPresignedURIConfig() {
288288
String putExpiry = properties.getProperty(AzureConstantsV12.PRESIGNED_HTTP_UPLOAD_URI_EXPIRY_SECONDS);
289289
if (putExpiry != null) {
290-
this.setHttpUploadURIExpirySeconds(Integer.parseInt(putExpiry));
290+
this.setHttpUploadURIExpirySeconds(capToDelegationKeyLifetime(Integer.parseInt(putExpiry)));
291291
}
292292
String getExpiry = properties.getProperty(AzureConstantsV12.PRESIGNED_HTTP_DOWNLOAD_URI_EXPIRY_SECONDS);
293293
if (getExpiry != null) {
294-
this.setHttpDownloadURIExpirySeconds(Integer.parseInt(getExpiry));
294+
this.setHttpDownloadURIExpirySeconds(capToDelegationKeyLifetime(Integer.parseInt(getExpiry)));
295295
String cacheMaxSize = properties.getProperty(AzureConstantsV12.PRESIGNED_HTTP_DOWNLOAD_URI_CACHE_MAX_SIZE);
296296
if (cacheMaxSize != null) {
297297
this.setHttpDownloadURICacheSize(Integer.parseInt(cacheMaxSize));
@@ -303,6 +303,25 @@ private void initPresignedURIConfig() {
303303
downloadDomainOverride = properties.getProperty(AzureConstantsV12.PRESIGNED_HTTP_DOWNLOAD_URI_DOMAIN_OVERRIDE, null);
304304
}
305305

306+
/**
307+
* When presigned URIs are signed with a user delegation key (service-principal auth), the SAS
308+
* expiry can never exceed the key's lifetime — Azure caps that at 7 days
309+
* ({@link AzureBlobContainerProviderV12#DELEGATION_KEY_LIFETIME}). A configured expiry beyond
310+
* that would defeat the delegation-key cache (never hits) and the SAS would silently stop
311+
* working before its stated expiry. Cap it here and warn instead of failing silently later.
312+
*/
313+
private int capToDelegationKeyLifetime(int configuredExpirySeconds) {
314+
long maxSeconds = AzureBlobContainerProviderV12.DELEGATION_KEY_LIFETIME.getSeconds();
315+
if (azureBlobContainerProvider.authenticateViaServicePrincipal() && configuredExpirySeconds > maxSeconds) {
316+
LOG.warn("Configured presigned URI expiry of {}s exceeds the {}s maximum lifetime of an Azure " +
317+
"user delegation key; capping to {}s to avoid a SAS URI that silently stops working " +
318+
"before its stated expiry.",
319+
configuredExpirySeconds, maxSeconds, maxSeconds);
320+
return (int) maxSeconds;
321+
}
322+
return configuredExpirySeconds;
323+
}
324+
306325
private void initReferenceKey() throws DataStoreException {
307326
// Set to false to defer key creation until the first upload — useful in read-only or cold-standby nodes
308327
// that should never write to blob storage during startup.
@@ -653,6 +672,11 @@ protected void setHttpDownloadURIExpirySeconds(int seconds) {
653672
httpDownloadURIExpirySeconds = seconds;
654673
}
655674

675+
// Package-private for test assertions on capped/parsed config values.
676+
int getHttpDownloadURIExpirySeconds() {
677+
return httpDownloadURIExpirySeconds;
678+
}
679+
656680
protected void setHttpDownloadURICacheSize(int maxSize) {
657681
// max size 0 or smaller is used to turn off the cache
658682
if (maxSize > 0) {
@@ -731,6 +755,11 @@ protected void setHttpUploadURIExpirySeconds(int seconds) {
731755
httpUploadURIExpirySeconds = seconds;
732756
}
733757

758+
// Package-private for test assertions on capped/parsed config values.
759+
int getHttpUploadURIExpirySeconds() {
760+
return httpUploadURIExpirySeconds;
761+
}
762+
734763
private DataIdentifier generateSafeRandomIdentifier() {
735764
return new DataIdentifier(
736765
String.format("%s-%d",

oak-blob-cloud-azure/src/main/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/v12/UtilsV12.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,16 @@ static ProxyOptions computeProxyOptions(@Nullable String proxyHost, @Nullable St
5858
public static RequestRetryOptions getRetryOptions(final String maxRequestRetryCount, Integer requestTimeout, String secondaryLocation) {
5959
int retries = PropertiesUtil.toInteger(maxRequestRetryCount, -1);
6060
if (retries < 0) {
61-
return null;
61+
if (secondaryLocation == null) {
62+
// No retry count and no secondary location configured — let the SDK apply its own defaults.
63+
return null;
64+
}
65+
// Secondary-location failover was explicitly enabled even though no retry count was
66+
// configured. Build options with the SDK's default retry count (maxTries=null → 4) so
67+
// the secondary host still takes effect instead of being silently dropped.
68+
return new RequestRetryOptions(RetryPolicyType.EXPONENTIAL, null,
69+
requestTimeout, null, null,
70+
secondaryLocation);
6271
}
6372

6473
if (retries == 0) {

oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/v12/AzureBlobStoreBackendV12CRUDTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,42 @@ public void init_downloadExpiryWithoutCacheSize_defaultsCacheToZero() throws Exc
409409
verify(container).exists();
410410
}
411411

412+
// 7 days, the hard limit of an Azure user delegation key's lifetime.
413+
private static final int DELEGATION_KEY_LIFETIME_SECONDS = 7 * 24 * 60 * 60;
414+
415+
@Test
416+
public void init_presignedExpiryExceedsDelegationKeyLifetime_withServicePrincipal_capsToMax() throws Exception {
417+
when(container.exists()).thenReturn(true);
418+
Properties p = baseInitProps();
419+
p.setProperty(AzureConstantsV12.AZURE_TENANT_ID, "tenant");
420+
p.setProperty(AzureConstantsV12.AZURE_CLIENT_ID, "client");
421+
p.setProperty(AzureConstantsV12.AZURE_CLIENT_SECRET, "secret");
422+
p.setProperty(AzureConstantsV12.PRESIGNED_HTTP_UPLOAD_URI_EXPIRY_SECONDS, String.valueOf(DELEGATION_KEY_LIFETIME_SECONDS + 100_000));
423+
p.setProperty(AzureConstantsV12.PRESIGNED_HTTP_DOWNLOAD_URI_EXPIRY_SECONDS, String.valueOf(DELEGATION_KEY_LIFETIME_SECONDS + 100_000));
424+
backend.setProperties(p);
425+
426+
backend.init();
427+
428+
assertEquals(DELEGATION_KEY_LIFETIME_SECONDS, backend.getHttpUploadURIExpirySeconds());
429+
assertEquals(DELEGATION_KEY_LIFETIME_SECONDS, backend.getHttpDownloadURIExpirySeconds());
430+
}
431+
432+
@Test
433+
public void init_presignedExpiryExceedsDelegationKeyLifetime_withoutServicePrincipal_notCapped() throws Exception {
434+
when(container.exists()).thenReturn(true);
435+
Properties p = baseInitProps();
436+
int uncappedExpiry = DELEGATION_KEY_LIFETIME_SECONDS + 100_000;
437+
p.setProperty(AzureConstantsV12.PRESIGNED_HTTP_UPLOAD_URI_EXPIRY_SECONDS, String.valueOf(uncappedExpiry));
438+
p.setProperty(AzureConstantsV12.PRESIGNED_HTTP_DOWNLOAD_URI_EXPIRY_SECONDS, String.valueOf(uncappedExpiry));
439+
backend.setProperties(p);
440+
441+
backend.init();
442+
443+
// No service-principal auth configured — SAS is not signed with a delegation key, so no cap applies.
444+
assertEquals(uncappedExpiry, backend.getHttpUploadURIExpirySeconds());
445+
assertEquals(uncappedExpiry, backend.getHttpDownloadURIExpirySeconds());
446+
}
447+
412448
// --- getAllIdentifiers / getAllRecords (list-based) ---
413449

414450
@SuppressWarnings("unchecked")

oak-blob-cloud-azure/src/test/java/org/apache/jackrabbit/oak/blob/cloud/azure/blobstorage/v12/UtilsV12Test.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.jackrabbit.oak.blob.cloud.azure.blobstorage.v12;
2020

21+
import com.azure.storage.common.policy.RequestRetryOptions;
2122
import org.junit.Test;
2223

2324
import java.io.IOException;
@@ -155,6 +156,19 @@ public void getRetryOptions_positiveCount_returnsNonNull() {
155156
assertNotNull(UtilsV12.getRetryOptions("3", null, null));
156157
}
157158

159+
/**
160+
* A secondary location alone (no explicit retry count) must not silently disable
161+
* secondary-location failover — options should still be built, using the SDK's default
162+
* retry count, with the secondary host set.
163+
*/
164+
@Test
165+
public void getRetryOptions_negativeCountWithSecondaryLocation_returnsOptionsWithSecondaryHost() {
166+
RequestRetryOptions options = UtilsV12.getRetryOptions("-1", null, "https://account-secondary.blob.core.windows.net");
167+
assertNotNull(options);
168+
assertEquals("https://account-secondary.blob.core.windows.net", options.getSecondaryHost());
169+
assertEquals(4, options.getMaxTries());
170+
}
171+
158172
@Test(expected = IOException.class)
159173
public void readConfig_nonExistentFile_throwsIOException() throws IOException {
160174
UtilsV12.readConfig("/tmp/does-not-exist-" + System.nanoTime() + ".properties");

0 commit comments

Comments
 (0)