diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index d98b4bc847a7..733ea20a9969 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_4ab10936db" + "Tag": "java/storage/azure-storage-blob_69330cd83d" } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java index b32ff69531f2..b1948f4df003 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java @@ -194,7 +194,8 @@ public void beforeTest() { new TestProxySanitizer("x-ms-copy-source-authorization", ".+", "REDACTED", TestProxySanitizerType.HEADER), new TestProxySanitizer("x-ms-rename-source", "((?<=http://|https://)([^/?]+)|sig=(.*))", "REDACTED", - TestProxySanitizerType.HEADER))); + TestProxySanitizerType.HEADER), + new TestProxySanitizer("skoid=([^&]+)", "REDACTED", TestProxySanitizerType.URL))); } // Ignore changes to the order of query parameters and wholly ignore the 'sv' (service version) query parameter diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java index 5bbb4c809db4..cdc576d7fb8b 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasAsyncClientTests.java @@ -50,10 +50,13 @@ import reactor.util.function.Tuples; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -583,6 +586,130 @@ public void containerSasFilterBlobsFail() { StepVerifier.create(client.setTags(tags)).verifyError(BlobStorageException.class); } + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-06") + public void createPermissionUpload() { + liveTestScenarioWithRetry(() -> { + BlobServiceAsyncClient oauthService = getOAuthServiceAsyncClient(); + BlobContainerAsyncClient oauthContainer + = oauthService.getBlobContainerAsyncClient(cc.getBlobContainerName()); + + String oauthBlobName = generateBlobName(); + OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); + + Mono response = oauthService.getUserDelegationKey(null, expiryTime).flatMap(key -> { + key.setSignedTenantId(testResourceNamer.recordValueFromConfig(key.getSignedTenantId())); + key.setSignedObjectId(testResourceNamer.recordValueFromConfig(key.getSignedObjectId())); + String saoid = testResourceNamer.randomUuid(); + + BlobSasPermission permissions = new BlobSasPermission().setCreatePermission(true); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setPreauthorizedAgentObjectId(saoid); + + String sasWithPermissions = oauthContainer.generateUserDelegationSas(sasValues, key); + + BlockBlobAsyncClient blockClient + = instrument(new SpecializedBlobClientBuilder().endpoint(oauthContainer.getBlobContainerUrl()) + .blobName(oauthBlobName) + .sasToken(sasWithPermissions)).buildBlockBlobAsyncClient(); + + return blockClient.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).then(); + }); + + StepVerifier.create(response).verifyComplete(); + }); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-06") + public void transferBlobWithCreatePermission() { + liveTestScenarioWithRetry(() -> { + BlobServiceAsyncClient oauthService = getOAuthServiceAsyncClient(); + String containerName = ccAsync.getBlobContainerName(); + BlobContainerAsyncClient oauthContainer = oauthService.getBlobContainerAsyncClient(containerName); + + String sourceBlobName = generateBlobName(); + String destinationBlobName = generateBlobName(); + OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); + + // Upload source blob via OAuth client + BlockBlobAsyncClient sourceBlob + = oauthContainer.getBlobAsyncClient(sourceBlobName).getBlockBlobAsyncClient(); + sourceBlob.upload(DATA.getDefaultFlux(), DATA.getDefaultDataSize()).block(); + + Mono response = oauthService.getUserDelegationKey(null, expiryTime).flatMap(key -> { + + key.setSignedTenantId(testResourceNamer.recordValueFromConfig(key.getSignedTenantId())); + key.setSignedObjectId(testResourceNamer.recordValueFromConfig(key.getSignedObjectId())); + String saoid = testResourceNamer.randomUuid(); + + // Create-only permission for destination blob + BlobSasPermission destinationPermissions = new BlobSasPermission().setCreatePermission(true); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, destinationPermissions) + .setPreauthorizedAgentObjectId(saoid); + String createPermissionsOnly = oauthContainer.generateUserDelegationSas(sasValues, key); + BlockBlobAsyncClient destinationClient + = instrument(new SpecializedBlobClientBuilder().endpoint(oauthContainer.getBlobContainerUrl()) + .blobName(destinationBlobName) + .sasToken(createPermissionsOnly)).buildBlockBlobAsyncClient(); + + // Read permission for source blob + BlobSasPermission readPermission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues readValues = new BlobServiceSasSignatureValues(expiryTime, readPermission) + .setPreauthorizedAgentObjectId(saoid); + String readSas = oauthContainer.generateUserDelegationSas(readValues, key); + String sourceUrl = sourceBlob.getBlobUrl() + "?" + readSas; + + return destinationClient.copyFromUrl(sourceUrl).then(); + }); + + StepVerifier.create(response).verifyComplete(); + }); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-06") + public void commitBlockListWithCreatePermission() { + liveTestScenarioWithRetry(() -> { + BlobServiceAsyncClient oauthService = getOAuthServiceAsyncClient(); + String containerName = ccAsync.getBlobContainerName(); + BlobContainerAsyncClient oauthContainer = oauthService.getBlobContainerAsyncClient(containerName); + String blockId = Base64.getEncoder().encodeToString("blockid".getBytes(StandardCharsets.UTF_8)); + List blockIds = new ArrayList<>(); + blockIds.add(blockId); + + String destinationBlobName = generateBlobName(); + OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); + + Mono response = oauthService.getUserDelegationKey(null, expiryTime).flatMap(key -> { + + key.setSignedTenantId(testResourceNamer.recordValueFromConfig(key.getSignedTenantId())); + key.setSignedObjectId(testResourceNamer.recordValueFromConfig(key.getSignedObjectId())); + String saoid = testResourceNamer.randomUuid(); + + // Create-only permission for destination blob + BlobSasPermission destinationPermissions = new BlobSasPermission().setCreatePermission(true); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, destinationPermissions) + .setPreauthorizedAgentObjectId(saoid); + String createPermissionsOnly = oauthContainer.generateUserDelegationSas(sasValues, key); + BlockBlobAsyncClient destinationClient + = instrument(new SpecializedBlobClientBuilder().endpoint(oauthContainer.getBlobContainerUrl()) + .blobName(destinationBlobName) + .sasToken(createPermissionsOnly)).buildBlockBlobAsyncClient(); + + Flux data = DATA.getDefaultFlux(); + + return destinationClient.stageBlock(blockId, data, DATA.getDefaultDataSize()) + .then(destinationClient.commitBlockList(blockIds, false)) + .then(); + }); + + StepVerifier.create(response).verifyComplete(); + }); + } + // RBAC replication lag @Test public void blobUserDelegationSaoid() { @@ -1484,4 +1611,5 @@ public void blobSasUserDelegationDelegatedTenantIdFail() { e -> assertExceptionStatusCodeAndMessage(e, 403, BlobErrorCode.AUTHENTICATION_FAILED)); }); } + } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java index 843fa2c8a125..47b3259e9539 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/SasClientTests.java @@ -41,10 +41,13 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -1296,6 +1299,115 @@ public void blobSasImplUtilCanonicalizedResource(String containerName, String bl assertEquals(expectedResource, queryParams.getResource()); } + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-06") + public void createPermissionUpload() { + liveTestScenarioWithRetry(() -> { + BlobServiceClient oauthService = getOAuthServiceClient(); + String oauthContainerName = cc.getBlobContainerName(); + BlobContainerClient oauthContainer = oauthService.getBlobContainerClient(oauthContainerName); + + String oauthBlobName = generateBlobName(); + OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); + + UserDelegationKey key = oauthService.getUserDelegationKey(null, expiryTime); + key.setSignedTenantId(testResourceNamer.recordValueFromConfig(key.getSignedTenantId())); + key.setSignedObjectId(testResourceNamer.recordValueFromConfig(key.getSignedObjectId())); + String saoid = testResourceNamer.randomUuid(); + + BlobSasPermission permissions = new BlobSasPermission().setCreatePermission(true); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, permissions).setPreauthorizedAgentObjectId(saoid); + + String sasWithPermissions = oauthContainer.generateUserDelegationSas(sasValues, key); + BlockBlobClient blockClient + = instrument(new SpecializedBlobClientBuilder().endpoint(oauthContainer.getBlobContainerUrl()) + .blobName(oauthBlobName) + .sasToken(sasWithPermissions)).buildBlockBlobClient(); + + assertDoesNotThrow(() -> blockClient.upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize())); + }); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-06") + public void transferBlobWithCreatePermission() { + liveTestScenarioWithRetry(() -> { + BlobServiceClient oauthService = getOAuthServiceClient(); + String containerName = cc.getBlobContainerName(); + BlobContainerClient oauthContainer = oauthService.getBlobContainerClient(containerName); + + String sourceBlobName = generateBlobName(); + String destinationBlobName = generateBlobName(); + OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); + + // Upload source blob via OAuth client + BlockBlobClient sourceBlob = oauthContainer.getBlobClient(sourceBlobName).getBlockBlobClient(); + sourceBlob.upload(DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + UserDelegationKey key = oauthService.getUserDelegationKey(null, expiryTime); + key.setSignedTenantId(testResourceNamer.recordValueFromConfig(key.getSignedTenantId())); + key.setSignedObjectId(testResourceNamer.recordValueFromConfig(key.getSignedObjectId())); + String saoid = testResourceNamer.randomUuid(); + + // Create-only permission for destination blob + BlobSasPermission destinationPermissions = new BlobSasPermission().setCreatePermission(true); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, destinationPermissions) + .setPreauthorizedAgentObjectId(saoid); + String createPermissionsOnly = oauthContainer.generateUserDelegationSas(sasValues, key); + BlockBlobClient destinationClient + = instrument(new SpecializedBlobClientBuilder().endpoint(oauthContainer.getBlobContainerUrl()) + .blobName(destinationBlobName) + .sasToken(createPermissionsOnly)).buildBlockBlobClient(); + + // Read permission for source blob + BlobSasPermission readPermission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues readValues + = new BlobServiceSasSignatureValues(expiryTime, readPermission).setPreauthorizedAgentObjectId(saoid); + String readSas = oauthContainer.generateUserDelegationSas(readValues, key); + String sourceUrl = sourceBlob.getBlobUrl() + "?" + readSas; + + assertDoesNotThrow(() -> destinationClient.copyFromUrl(sourceUrl)); + }); + } + + @Test + @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-04-06") + public void commitBlockListWithCreatePermission() { + liveTestScenarioWithRetry(() -> { + BlobServiceClient oauthService = getOAuthServiceClient(); + String containerName = cc.getBlobContainerName(); + BlobContainerClient oauthContainer = oauthService.getBlobContainerClient(containerName); + String blockId = Base64.getEncoder().encodeToString("blockid".getBytes(StandardCharsets.UTF_8)); + List blockIds = new ArrayList<>(); + blockIds.add(blockId); + + String destinationBlobName = generateBlobName(); + OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1); + + UserDelegationKey key = oauthService.getUserDelegationKey(null, expiryTime); + key.setSignedTenantId(testResourceNamer.recordValueFromConfig(key.getSignedTenantId())); + key.setSignedObjectId(testResourceNamer.recordValueFromConfig(key.getSignedObjectId())); + String saoid = testResourceNamer.randomUuid(); + + // Create-only permission for destination blob + BlobSasPermission destinationPermissions = new BlobSasPermission().setCreatePermission(true); + BlobServiceSasSignatureValues sasValues + = new BlobServiceSasSignatureValues(expiryTime, destinationPermissions) + .setPreauthorizedAgentObjectId(saoid); + String createPermissionsOnly = oauthContainer.generateUserDelegationSas(sasValues, key); + BlockBlobClient destinationClient + = instrument(new SpecializedBlobClientBuilder().endpoint(oauthContainer.getBlobContainerUrl()) + .blobName(destinationBlobName) + .sasToken(createPermissionsOnly)).buildBlockBlobClient(); + + destinationClient.stageBlock(blockId, DATA.getDefaultInputStream(), DATA.getDefaultDataSize()); + + assertDoesNotThrow(() -> destinationClient.commitBlockList(blockIds, false)); + }); + } + private static Stream blobSasImplUtilCanonicalizedResourceSupplier() { return Stream.of( Arguments.of("c", "b", "id", OffsetDateTime.now(), "bs",